Browse Source

fix ABID generation by chopping ts_src precision to consistent length

Nick Sweeting 1 year ago
parent
commit
4427869ae8

+ 8 - 3
archivebox/abid_utils/abid.py

@@ -114,7 +114,7 @@ class ABID(NamedTuple):
 @enforce_types
 @enforce_types
 def uri_hash(uri: Union[str, bytes], salt: str=DEFAULT_ABID_URI_SALT) -> str:
 def uri_hash(uri: Union[str, bytes], salt: str=DEFAULT_ABID_URI_SALT) -> str:
     """
     """
-    'E4A5CCD9AF4ED2A6E0954DF19FD274E9CDDB4853051F033FD518BFC90AA1AC25'
+    https://example.com -> 'E4A5CCD9AF4ED2A6E0954DF19FD274E9CDDB4853051F033FD518BFC90AA1AC25' (example.com)
     """
     """
     if isinstance(uri, bytes):
     if isinstance(uri, bytes):
         uri_str: str = uri.decode()
         uri_str: str = uri.decode()
@@ -130,6 +130,7 @@ def uri_hash(uri: Union[str, bytes], salt: str=DEFAULT_ABID_URI_SALT) -> str:
         except AttributeError:
         except AttributeError:
             pass
             pass
     
     
+    # the uri hash is the sha256 of the domain + salt
     uri_bytes = uri_str.encode('utf-8') + salt.encode('utf-8')
     uri_bytes = uri_str.encode('utf-8') + salt.encode('utf-8')
 
 
     return hashlib.sha256(uri_bytes).hexdigest().upper()
     return hashlib.sha256(uri_bytes).hexdigest().upper()
@@ -162,7 +163,11 @@ def abid_part_from_ts(ts: datetime) -> str:
     return str(ulid.from_timestamp(ts))[:ABID_TS_LEN]
     return str(ulid.from_timestamp(ts))[:ABID_TS_LEN]
 
 
 @enforce_types
 @enforce_types
-def abid_part_from_subtype(subtype: str) -> str:
+def ts_from_abid(abid: str) -> datetime:
+    return ulid.parse(abid.split('_', 1)[-1]).timestamp().datetime
+
+@enforce_types
+def abid_part_from_subtype(subtype: str | int) -> str:
     """
     """
     Snapshots have 01 type, other objects have other subtypes like wget/media/etc.
     Snapshots have 01 type, other objects have other subtypes like wget/media/etc.
     Also allows us to change the ulid spec later by putting special sigil values here.
     Also allows us to change the ulid spec later by putting special sigil values here.
@@ -196,7 +201,7 @@ def abid_part_from_rand(rand: Union[str, UUID, None, int]) -> str:
 
 
 
 
 @enforce_types
 @enforce_types
-def abid_hashes_from_values(prefix: str, ts: datetime, uri: str, subtype: str, rand: Union[str, UUID, None, int], salt: str=DEFAULT_ABID_URI_SALT) -> Dict[str, str]:
+def abid_hashes_from_values(prefix: str, ts: datetime, uri: str, subtype: str | int, rand: Union[str, UUID, None, int], salt: str=DEFAULT_ABID_URI_SALT) -> Dict[str, str]:
     return {
     return {
         'prefix': abid_part_from_prefix(prefix),
         'prefix': abid_part_from_prefix(prefix),
         'ts': abid_part_from_ts(ts),
         'ts': abid_part_from_ts(ts),

+ 15 - 14
archivebox/abid_utils/admin.py

@@ -27,7 +27,7 @@ def get_abid_info(self, obj, request=None):
     try:
     try:
         abid_diff = f' != obj.ABID: {highlight_diff(obj.ABID, obj.abid)} ❌' if str(obj.ABID) != str(obj.abid) else ' == .ABID ✅'
         abid_diff = f' != obj.ABID: {highlight_diff(obj.ABID, obj.abid)} ❌' if str(obj.ABID) != str(obj.abid) else ' == .ABID ✅'
 
 
-        fresh_abid = obj.ABID_FRESH
+        fresh_abid = obj.ABID
         fresh_abid_diff = f' !=   .fresh_abid: {highlight_diff(fresh_abid, obj.ABID)} ❌' if str(fresh_abid) != str(obj.ABID) else '✅'
         fresh_abid_diff = f' !=   .fresh_abid: {highlight_diff(fresh_abid, obj.ABID)} ❌' if str(fresh_abid) != str(obj.ABID) else '✅'
         fresh_uuid_diff = f' !=   .fresh_uuid: {highlight_diff(fresh_abid.uuid, obj.ABID.uuid)} ❌' if str(fresh_abid.uuid) != str(obj.ABID.uuid) else '✅'
         fresh_uuid_diff = f' !=   .fresh_uuid: {highlight_diff(fresh_abid.uuid, obj.ABID.uuid)} ❌' if str(fresh_abid.uuid) != str(obj.ABID.uuid) else '✅'
 
 
@@ -35,17 +35,17 @@ def get_abid_info(self, obj, request=None):
         id_abid_diff = f' !=  .abid.uuid: {highlight_diff(obj.ABID.uuid, obj.id)} ❌' if str(obj.id) != str(obj.ABID.uuid) else ' == .abid ✅'
         id_abid_diff = f' !=  .abid.uuid: {highlight_diff(obj.ABID.uuid, obj.id)} ❌' if str(obj.id) != str(obj.ABID.uuid) else ' == .abid ✅'
         id_pk_diff = f' !=  .pk: {highlight_diff(obj.pk, obj.id)} ❌' if str(obj.pk) != str(obj.id) else ' ==  .pk ✅'
         id_pk_diff = f' !=  .pk: {highlight_diff(obj.pk, obj.id)} ❌' if str(obj.pk) != str(obj.id) else ' ==  .pk ✅'
 
 
-        source_ts_val = parse_date(obj.abid_values['ts']) or None
-        derived_ts = abid_part_from_ts(source_ts_val) if source_ts_val else None
+        fresh_ts = parse_date(obj.ABID_FRESH_VALUES['ts']) or None
+        derived_ts = abid_part_from_ts(fresh_ts) if fresh_ts else None
         ts_diff = f'!= {highlight_diff(derived_ts, obj.ABID.ts)} ❌' if derived_ts != obj.ABID.ts else '✅'
         ts_diff = f'!= {highlight_diff(derived_ts, obj.ABID.ts)} ❌' if derived_ts != obj.ABID.ts else '✅'
 
 
-        derived_uri = abid_part_from_uri(obj.abid_values['uri'])
+        derived_uri = abid_part_from_uri(obj.ABID_FRESH_VALUES['uri'])
         uri_diff = f'!= {highlight_diff(derived_uri, obj.ABID.uri)} ❌' if derived_uri != obj.ABID.uri else '✅'
         uri_diff = f'!= {highlight_diff(derived_uri, obj.ABID.uri)} ❌' if derived_uri != obj.ABID.uri else '✅'
 
 
-        derived_subtype = abid_part_from_subtype(obj.abid_values['subtype'])
+        derived_subtype = abid_part_from_subtype(obj.ABID_FRESH_VALUES['subtype'])
         subtype_diff = f'!= {highlight_diff(derived_subtype, obj.ABID.subtype)} ❌' if derived_subtype != obj.ABID.subtype else '✅'
         subtype_diff = f'!= {highlight_diff(derived_subtype, obj.ABID.subtype)} ❌' if derived_subtype != obj.ABID.subtype else '✅'
 
 
-        derived_rand = abid_part_from_rand(obj.abid_values['rand'])
+        derived_rand = abid_part_from_rand(obj.ABID_FRESH_VALUES['rand'])
         rand_diff = f'!= {highlight_diff(derived_rand, obj.ABID.rand)} ❌' if derived_rand != obj.ABID.rand else '✅'
         rand_diff = f'!= {highlight_diff(derived_rand, obj.ABID.rand)} ❌' if derived_rand != obj.ABID.rand else '✅'
 
 
         # any_abid_discrepancies = any(
         # any_abid_discrepancies = any(
@@ -60,9 +60,9 @@ def get_abid_info(self, obj, request=None):
             <a href="{}" style="font-size: 16px; font-family: monospace; user-select: all; border-radius: 8px; background-color: #ddf; padding: 3px 5px; border: 1px solid #aaa; margin-bottom: 8px; display: inline-block; vertical-align: top;">{}</a> &nbsp; &nbsp; <a href="{}" style="color: limegreen; font-size: 0.9em; vertical-align: 1px; font-family: monospace;">📖 API DOCS</a>
             <a href="{}" style="font-size: 16px; font-family: monospace; user-select: all; border-radius: 8px; background-color: #ddf; padding: 3px 5px; border: 1px solid #aaa; margin-bottom: 8px; display: inline-block; vertical-align: top;">{}</a> &nbsp; &nbsp; <a href="{}" style="color: limegreen; font-size: 0.9em; vertical-align: 1px; font-family: monospace;">📖 API DOCS</a>
             <br/><hr/>
             <br/><hr/>
             <div style="opacity: 0.8">
             <div style="opacity: 0.8">
-            &nbsp; &nbsp; <small style="opacity: 0.8">.abid: &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; <code style="font-size: 10px; user-select: all">{}</code> &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; {}</small><br/>
-            &nbsp; &nbsp; <small style="opacity: 0.8">.abid.uuid: &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; <code style="font-size: 10px; user-select: all">{}</code> &nbsp; &nbsp; {}</small><br/>
             &nbsp; &nbsp; <small style="opacity: 0.8">.id: &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;<code style="font-size: 10px; user-select: all">{}</code> &nbsp; &nbsp; {}</small><br/>
             &nbsp; &nbsp; <small style="opacity: 0.8">.id: &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;<code style="font-size: 10px; user-select: all">{}</code> &nbsp; &nbsp; {}</small><br/>
+            &nbsp; &nbsp; <small style="opacity: 0.8">.abid.uuid: &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; <code style="font-size: 10px; user-select: all">{}</code> &nbsp; &nbsp; {}</small><br/>
+            &nbsp; &nbsp; <small style="opacity: 0.8">.abid: &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; <code style="font-size: 10px; user-select: all">{}</code> &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; {}</small><br/>
             <hr/>
             <hr/>
             &nbsp; &nbsp; TS: &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;<code style="font-size: 10px;"><b style="user-select: all">{}</b> &nbsp; {}</code> &nbsp; &nbsp; &nbsp;&nbsp; <code style="font-size: 10px;"><b>{}</b></code> {}: <code style="user-select: all">{}</code><br/>
             &nbsp; &nbsp; TS: &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;<code style="font-size: 10px;"><b style="user-select: all">{}</b> &nbsp; {}</code> &nbsp; &nbsp; &nbsp;&nbsp; <code style="font-size: 10px;"><b>{}</b></code> {}: <code style="user-select: all">{}</code><br/>
             &nbsp; &nbsp; URI: &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; <code style="font-size: 10px;"><b style="user-select: all">{}</b> &nbsp; &nbsp; {}</code> &nbsp;&nbsp; &nbsp; &nbsp; &nbsp;&nbsp; <code style="font-size: 10px;"><b>{}</b></code> <span style="display:inline-block; vertical-align: -4px; width: 330px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">{}: <code style="user-select: all">{}</code></span><br/>
             &nbsp; &nbsp; URI: &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; <code style="font-size: 10px;"><b style="user-select: all">{}</b> &nbsp; &nbsp; {}</code> &nbsp;&nbsp; &nbsp; &nbsp; &nbsp;&nbsp; <code style="font-size: 10px;"><b>{}</b></code> <span style="display:inline-block; vertical-align: -4px; width: 330px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">{}: <code style="user-select: all">{}</code></span><br/>
@@ -73,15 +73,15 @@ def get_abid_info(self, obj, request=None):
             </div>
             </div>
             ''',
             ''',
             obj.api_url + (f'?api_key={get_or_create_api_token(request.user)}' if request and request.user else ''), obj.api_url, obj.api_docs_url,
             obj.api_url + (f'?api_key={get_or_create_api_token(request.user)}' if request and request.user else ''), obj.api_url, obj.api_docs_url,
+            highlight_diff(obj.id, obj.ABID.uuid), mark_safe(id_pk_diff + id_abid_diff),
+            highlight_diff(obj.ABID.uuid, obj.id), mark_safe(fresh_uuid_diff),
             highlight_diff(obj.abid, fresh_abid), mark_safe(fresh_abid_diff),
             highlight_diff(obj.abid, fresh_abid), mark_safe(fresh_abid_diff),
-            highlight_diff(obj.ABID.uuid, fresh_abid.uuid), mark_safe(fresh_uuid_diff),
-            str(obj.id), mark_safe(id_pk_diff + id_abid_diff + id_fresh_abid_diff),
             # str(fresh_abid.uuid), mark_safe(fresh_uuid_diff),
             # str(fresh_abid.uuid), mark_safe(fresh_uuid_diff),
             # str(fresh_abid), mark_safe(fresh_abid_diff),
             # str(fresh_abid), mark_safe(fresh_abid_diff),
-            highlight_diff(obj.ABID.ts, derived_ts), highlight_diff(str(obj.ABID.uuid)[0:14], str(fresh_abid.uuid)[0:14]), mark_safe(ts_diff), obj.abid_ts_src, source_ts_val and source_ts_val.isoformat(),
-            highlight_diff(obj.ABID.uri, derived_uri), highlight_diff(str(obj.ABID.uuid)[14:26], str(fresh_abid.uuid)[14:26]), mark_safe(uri_diff), obj.abid_uri_src, str(obj.abid_values['uri']),
-            highlight_diff(obj.ABID.subtype, derived_subtype), highlight_diff(str(obj.ABID.uuid)[26:28], str(fresh_abid.uuid)[26:28]), mark_safe(subtype_diff), obj.abid_subtype_src, str(obj.abid_values['subtype']),
-            highlight_diff(obj.ABID.rand, derived_rand), highlight_diff(str(obj.ABID.uuid)[28:36], str(fresh_abid.uuid)[28:36]), mark_safe(rand_diff), obj.abid_rand_src, str(obj.abid_values['rand'])[-7:],
+            highlight_diff(obj.ABID.ts, derived_ts), highlight_diff(str(obj.ABID.uuid)[0:14], str(fresh_abid.uuid)[0:14]), mark_safe(ts_diff), obj.abid_ts_src, fresh_ts and fresh_ts.isoformat(),
+            highlight_diff(obj.ABID.uri, derived_uri), highlight_diff(str(obj.ABID.uuid)[14:26], str(fresh_abid.uuid)[14:26]), mark_safe(uri_diff), obj.abid_uri_src, str(obj.ABID_FRESH_VALUES['uri']),
+            highlight_diff(obj.ABID.subtype, derived_subtype), highlight_diff(str(obj.ABID.uuid)[26:28], str(fresh_abid.uuid)[26:28]), mark_safe(subtype_diff), obj.abid_subtype_src, str(obj.ABID_FRESH_VALUES['subtype']),
+            highlight_diff(obj.ABID.rand, derived_rand), highlight_diff(str(obj.ABID.uuid)[28:36], str(fresh_abid.uuid)[28:36]), mark_safe(rand_diff), obj.abid_rand_src, str(obj.ABID_FRESH_VALUES['rand'])[-7:],
             highlight_diff(getattr(obj, 'old_id', ''), obj.pk),
             highlight_diff(getattr(obj, 'old_id', ''), obj.pk),
         )
         )
     except Exception as e:
     except Exception as e:
@@ -93,6 +93,7 @@ class ABIDModelAdmin(admin.ModelAdmin):
     sort_fields = ('created', 'created_by', 'abid', '__str__')
     sort_fields = ('created', 'created_by', 'abid', '__str__')
     readonly_fields = ('created', 'modified', '__str__', 'API')
     readonly_fields = ('created', 'modified', '__str__', 'API')
 
 
+    @admin.display(description='API Identifiers')
     def API(self, obj):
     def API(self, obj):
         return get_abid_info(self, obj, request=self.request)
         return get_abid_info(self, obj, request=self.request)
 
 

+ 87 - 75
archivebox/abid_utils/models.py

@@ -1,7 +1,5 @@
 """
 """
 This file provides the Django ABIDField and ABIDModel base model to inherit from.
 This file provides the Django ABIDField and ABIDModel base model to inherit from.
-
-It implements the ArchiveBox ID (ABID) interfaces including abid_values, generate_abid, .abid, .uuid, .id.
 """
 """
 
 
 from typing import Any, Dict, Union, List, Set, NamedTuple, cast
 from typing import Any, Dict, Union, List, Set, NamedTuple, cast
@@ -9,7 +7,7 @@ from typing import Any, Dict, Union, List, Set, NamedTuple, cast
 from ulid import ULID
 from ulid import ULID
 from uuid import uuid4, UUID
 from uuid import uuid4, UUID
 from typeid import TypeID            # type: ignore[import-untyped]
 from typeid import TypeID            # type: ignore[import-untyped]
-from datetime import datetime
+from datetime import datetime, timedelta
 from functools import partial
 from functools import partial
 from charidfield import CharIDField  # type: ignore[import-untyped]
 from charidfield import CharIDField  # type: ignore[import-untyped]
 
 
@@ -30,7 +28,10 @@ from .abid import (
     DEFAULT_ABID_PREFIX,
     DEFAULT_ABID_PREFIX,
     DEFAULT_ABID_URI_SALT,
     DEFAULT_ABID_URI_SALT,
     abid_part_from_prefix,
     abid_part_from_prefix,
-    abid_from_values
+    abid_hashes_from_values,
+    abid_from_values,
+    ts_from_abid,
+    abid_part_from_ts,
 )
 )
 
 
 ####################################################
 ####################################################
@@ -63,134 +64,141 @@ def get_or_create_system_user_pk(username='system'):
 
 
 
 
 class AutoDateTimeField(models.DateTimeField):
 class AutoDateTimeField(models.DateTimeField):
-    def pre_save(self, model_instance, add):
-        return timezone.now()
+    # def pre_save(self, model_instance, add):
+    #     return timezone.now()
+    pass
 
 
 
 
 class ABIDModel(models.Model):
 class ABIDModel(models.Model):
     """
     """
     Abstract Base Model for other models to depend on. Provides ArchiveBox ID (ABID) interface.
     Abstract Base Model for other models to depend on. Provides ArchiveBox ID (ABID) interface.
     """
     """
-    abid_prefix: str = DEFAULT_ABID_PREFIX  # e.g. 'tag_'
+    abid_prefix: str = DEFAULT_ABID_PREFIX   # e.g. 'tag_'
     abid_ts_src = 'None'                    # e.g. 'self.created'
     abid_ts_src = 'None'                    # e.g. 'self.created'
     abid_uri_src = 'None'                   # e.g. 'self.uri'
     abid_uri_src = 'None'                   # e.g. 'self.uri'
     abid_subtype_src = 'None'               # e.g. 'self.extractor'
     abid_subtype_src = 'None'               # e.g. 'self.extractor'
     abid_rand_src = 'None'                  # e.g. 'self.uuid' or 'self.id'
     abid_rand_src = 'None'                  # e.g. 'self.uuid' or 'self.id'
+    abid_salt: str = DEFAULT_ABID_URI_SALT
 
 
     # id = models.UUIDField(primary_key=True, default=uuid4, editable=True)
     # id = models.UUIDField(primary_key=True, default=uuid4, editable=True)
     # uuid = models.UUIDField(blank=True, null=True, editable=True, unique=True)
     # uuid = models.UUIDField(blank=True, null=True, editable=True, unique=True)
     abid = ABIDField(prefix=abid_prefix)
     abid = ABIDField(prefix=abid_prefix)
 
 
     created_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, default=get_or_create_system_user_pk)
     created_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, default=get_or_create_system_user_pk)
-    created = AutoDateTimeField(default=timezone.now, db_index=True)
+    created = AutoDateTimeField(default=None, null=False, db_index=True)
     modified = models.DateTimeField(auto_now=True)
     modified = models.DateTimeField(auto_now=True)
 
 
     class Meta(TypedModelMeta):
     class Meta(TypedModelMeta):
         abstract = True
         abstract = True
 
 
     def save(self, *args: Any, **kwargs: Any) -> None:
     def save(self, *args: Any, **kwargs: Any) -> None:
-        self.created = self.created or timezone.now()
-
-        assert all(val for val in self.abid_values.values()), f'All ABID src values must be set: {self.abid_values}'
-
         if self._state.adding:
         if self._state.adding:
-            self.id = self.ABID.uuid
-            self.abid = str(self.ABID)
-        else:
-            assert self.id, 'id must be set when object exists in DB'
-            if not self.abid:
-                self.abid = str(self.ABID)
-        #     assert str(self.abid) == str(self.ABID), f'self.abid {self.id} does not match self.ABID {self.ABID.uuid}'
-
-        # fresh_abid = self.generate_abid()
-        # if str(fresh_abid) != str(self.abid):
-        #     self.abid = str(fresh_abid)
-
+            self.issue_new_abid()
         return super().save(*args, **kwargs)
         return super().save(*args, **kwargs)
 
 
-        assert str(self.id) == str(self.ABID.uuid), f'self.id {self.id} does not match self.ABID {self.ABID.uuid}'
-        assert str(self.abid) == str(self.ABID), f'self.abid {self.id} does not match self.ABID {self.ABID.uuid}'
-        assert str(self.uuid) == str(self.ABID.uuid), f'self.uuid ({self.uuid}) does not match .ABID.uuid ({self.ABID.uuid})'
+        # assert str(self.id) == str(self.ABID.uuid), f'self.id {self.id} does not match self.ABID {self.ABID.uuid}'
+        # assert str(self.abid) == str(self.ABID), f'self.abid {self.id} does not match self.ABID {self.ABID.uuid}'
+        # assert str(self.uuid) == str(self.ABID.uuid), f'self.uuid ({self.uuid}) does not match .ABID.uuid ({self.ABID.uuid})'
 
 
     @property
     @property
-    def abid_values(self) -> Dict[str, Any]:
+    def ABID_FRESH_VALUES(self) -> Dict[str, Any]:
+        assert self.abid_ts_src != 'None'
+        assert self.abid_uri_src != 'None'
+        assert self.abid_rand_src != 'None'
+        assert self.abid_subtype_src != 'None'
         return {
         return {
             'prefix': self.abid_prefix,
             'prefix': self.abid_prefix,
             'ts': eval(self.abid_ts_src),
             'ts': eval(self.abid_ts_src),
             'uri': eval(self.abid_uri_src),
             'uri': eval(self.abid_uri_src),
             'subtype': eval(self.abid_subtype_src),
             'subtype': eval(self.abid_subtype_src),
             'rand': eval(self.abid_rand_src),
             'rand': eval(self.abid_rand_src),
+            'salt': self.abid_salt,
         }
         }
+    
+    @property
+    def ABID_FRESH_HASHES(self) -> Dict[str, str]:
+        return abid_hashes_from_values(**self.ABID_FRESH_VALUES)
 
 
-    def generate_abid(self) -> ABID:
+    
+    @property
+    def ABID_FRESH(self) -> ABID:
         """
         """
-        Return a freshly derived ABID (assembled from attrs defined in ABIDModel.abid_*_src).
+        Return a pure freshly derived ABID (assembled from attrs defined in ABIDModel.abid_*_src).
         """
         """
-        prefix, ts, uri, subtype, rand = self.abid_values.values()
-
-        if (not prefix) or prefix == DEFAULT_ABID_PREFIX:
-            suggested_abid = self.__class__.__name__[:3].lower()
-            raise Exception(f'{self.__class__.__name__}.abid_prefix must be defined to calculate ABIDs (suggested: {suggested_abid})')
-
-        if not ts:
-            # default to unix epoch with 00:00:00 UTC
-            ts = datetime.fromtimestamp(0, timezone.utc)     # equivalent to: ts = datetime.utcfromtimestamp(0)
-            print(f'[!] WARNING: Generating ABID with ts=0000000000 placeholder because {self.__class__.__name__}.abid_ts_src={self.abid_ts_src} is unset!', ts.isoformat())
-
-        if not uri:
-            uri = str(self)
-            print(f'[!] WARNING: Generating ABID with uri=str(self) placeholder because {self.__class__.__name__}.abid_uri_src={self.abid_uri_src} is unset!', uri)
-
-        if not subtype:
-            subtype = self.__class__.__name__
-            print(f'[!] WARNING: Generating ABID with subtype={subtype} placeholder because {self.__class__.__name__}.abid_subtype_src={self.abid_subtype_src} is unset!', subtype)
-
-        if not rand:
-            rand = getattr(self, 'uuid', None) or getattr(self, 'id', None) or getattr(self, 'pk')
-            print(f'[!] WARNING: Generating ABID with rand=self.id placeholder because {self.__class__.__name__}.abid_rand_src={self.abid_rand_src} is unset!', rand)
-
-        abid = abid_from_values(
-            prefix=prefix,
-            ts=ts,
-            uri=uri,
-            subtype=subtype,
-            rand=rand,
-            salt=DEFAULT_ABID_URI_SALT,
-        )
-        assert abid.ulid and abid.uuid and abid.typeid, f'Failed to calculate {prefix}_ABID for {self.__class__.__name__}'
+
+        abid_fresh_values = self.ABID_FRESH_VALUES
+        assert all(abid_fresh_values.values()), f'All ABID_FRESH_VALUES must be set {abid_fresh_values}'
+        abid_fresh_hashes = self.ABID_FRESH_HASHES
+        assert all(abid_fresh_hashes.values()), f'All ABID_FRESH_HASHES must be able to be generated {abid_fresh_hashes}'
+        
+        abid = ABID(**abid_fresh_hashes)
+        
+        assert abid.ulid and abid.uuid and abid.typeid, f'Failed to calculate {abid_fresh_values["prefix"]}_ABID for {self.__class__.__name__}'
         return abid
         return abid
 
 
+
+    def issue_new_abid(self):
+        assert self.abid is None, f'Can only issue new ABID for new objects that dont already have one {self.abid}'
+        assert self._state.adding, 'Can only issue new ABID when model._state.adding is True'
+        assert eval(self.abid_uri_src), f'Can only issue new ABID if self.abid_uri_src is defined ({self.abid_uri_src}={eval(self.abid_uri_src)})'
+
+        self.old_id = getattr(self, 'old_id', None) or self.id or uuid4()
+        self.abid = None
+        self.created = ts_from_abid(abid_part_from_ts(timezone.now()))  # cut off precision to match precision of TS component
+        self.added = getattr(self, 'added', None) or self.created
+        self.modified = self.created
+        abid_ts_src_attr = self.abid_ts_src.split('self.', 1)[-1]   # e.g. 'self.added' -> 'added'
+        if abid_ts_src_attr and abid_ts_src_attr != 'created' and hasattr(self, abid_ts_src_attr):
+            # self.added = self.created
+            existing_abid_ts = getattr(self, abid_ts_src_attr, None)
+            created_and_abid_ts_are_same = existing_abid_ts and (existing_abid_ts - self.created) < timedelta(seconds=5)
+            if created_and_abid_ts_are_same:
+                setattr(self, abid_ts_src_attr, self.created)
+                assert getattr(self, abid_ts_src_attr) == self.created
+
+        assert all(self.ABID_FRESH_VALUES.values()), f'Can only issue new ABID if all self.ABID_FRESH_VALUES are defined {self.ABID_FRESH_VALUES}'
+
+        new_abid = self.ABID_FRESH
+
+        # store stable ABID on local fields, overwrite them because we are adding a new entry and existing defaults havent touched db yet
+        self.abid = str(new_abid)
+        self.id = new_abid.uuid
+        self.pk = new_abid.uuid
+
+        assert self.ABID == new_abid
+        assert str(self.ABID.uuid) == str(self.id) == str(self.pk) == str(ABID.parse(self.abid).uuid)
+        
+        self._ready_to_save_as_new = True
+
+
     @property
     @property
     def ABID(self) -> ABID:
     def ABID(self) -> ABID:
         """
         """
-        ULIDParts(timestamp='01HX9FPYTR', url='E4A5CCD9', subtype='00', randomness='ZYEBQE')
+        aka get_or_generate_abid -> ULIDParts(timestamp='01HX9FPYTR', url='E4A5CCD9', subtype='00', randomness='ZYEBQE')
         """
         """
-        
-        # if object is not yet saved to DB, always generate fresh ABID from values
-        if self._state.adding:
-            return self.generate_abid()
-        
+
         # otherwise DB is single source of truth, load ABID from existing db pk
         # otherwise DB is single source of truth, load ABID from existing db pk
         abid: ABID | None = None
         abid: ABID | None = None
         try:
         try:
-            abid = abid or ABID.parse(self.pk)
+            abid = abid or ABID.parse(cast(str, self.abid))
         except Exception:
         except Exception:
             pass
             pass
 
 
         try:
         try:
-            abid = abid or ABID.parse(self.id)
+            abid = abid or ABID.parse(cast(str, self.id))
         except Exception:
         except Exception:
             pass
             pass
 
 
         try:
         try:
-            abid = abid or ABID.parse(cast(str, self.abid))
+            abid = abid or ABID.parse(cast(str, self.pk))
         except Exception:
         except Exception:
             pass
             pass
 
 
-        abid = abid or self.generate_abid()
+        abid = abid or self.ABID_FRESH
 
 
         return abid
         return abid
 
 
+
     @property
     @property
     def ULID(self) -> ULID:
     def ULID(self) -> ULID:
         """
         """
@@ -210,8 +218,7 @@ class ABIDModel(models.Model):
         """
         """
         Get a str uuid.UUID (v4) representation of the object's ABID.
         Get a str uuid.UUID (v4) representation of the object's ABID.
         """
         """
-        assert str(self.id) == str(self.ABID.uuid)
-        return str(self.id)
+        return str(self.ABID.uuid)
 
 
     @property
     @property
     def TypeID(self) -> TypeID:
     def TypeID(self) -> TypeID:
@@ -220,6 +227,10 @@ class ABIDModel(models.Model):
         """
         """
         return self.ABID.typeid
         return self.ABID.typeid
     
     
+    @property
+    def abid_uri(self) -> str:
+        return eval(self.abid_uri_src)
+    
     @property
     @property
     def api_url(self) -> str:
     def api_url(self) -> str:
         # /api/v1/core/any/{abid}
         # /api/v1/core/any/{abid}
@@ -290,6 +301,7 @@ def find_obj_from_abid_rand(rand: Union[ABID, str], model=None) -> List[ABIDMode
     """
     """
     Find an object corresponding to an ABID by exhaustively searching using its random suffix (slow).
     Find an object corresponding to an ABID by exhaustively searching using its random suffix (slow).
     e.g. 'obj_....................JYRPAQ' -> Snapshot('snp_01BJQMF54D093DXEAWZ6JYRPAQ')
     e.g. 'obj_....................JYRPAQ' -> Snapshot('snp_01BJQMF54D093DXEAWZ6JYRPAQ')
+    Honestly should only be used for debugging, no reason to expose this ability to users.
     """
     """
 
 
     # convert str to ABID if necessary
     # convert str to ABID if necessary
@@ -339,7 +351,7 @@ def find_obj_from_abid_rand(rand: Union[ABID, str], model=None) -> List[ABIDMode
                 )
                 )
 
 
             for obj in qs:
             for obj in qs:
-                if obj.generate_abid() == abid:
+                if abid in (str(obj.ABID_FRESH), str(obj.id), str(obj.abid)):
                     # found exact match, no need to keep iterating
                     # found exact match, no need to keep iterating
                     return [obj]
                     return [obj]
                 partial_matches.append(obj)
                 partial_matches.append(obj)

+ 2 - 2
archivebox/core/admin.py

@@ -353,10 +353,10 @@ class SnapshotActionForm(ActionForm):
 class SnapshotAdmin(SearchResultsAdminMixin, ABIDModelAdmin):
 class SnapshotAdmin(SearchResultsAdminMixin, ABIDModelAdmin):
     list_display = ('added', 'title_str', 'files', 'size', 'url_str')
     list_display = ('added', 'title_str', 'files', 'size', 'url_str')
     sort_fields = ('title_str', 'url_str', 'added')
     sort_fields = ('title_str', 'url_str', 'added')
-    readonly_fields = ('tags_str', 'timestamp', 'admin_actions', 'status_info', 'bookmarked', 'added', 'updated', 'created', 'modified', 'API', 'link_dir')
+    readonly_fields = ('tags_str', 'timestamp', 'admin_actions', 'status_info', 'bookmarked', 'updated', 'created', 'modified', 'API', 'link_dir')
     search_fields = ('id', 'url', 'abid', 'old_id', 'timestamp', 'title', 'tags__name')
     search_fields = ('id', 'url', 'abid', 'old_id', 'timestamp', 'title', 'tags__name')
     list_filter = ('added', 'updated', 'archiveresult__status', 'created_by', 'tags__name')
     list_filter = ('added', 'updated', 'archiveresult__status', 'created_by', 'tags__name')
-    fields = ('url', 'created_by', 'title', *readonly_fields)
+    fields = ('url', 'created_by', 'title', 'added', *readonly_fields)
     ordering = ['-added']
     ordering = ['-added']
     actions = ['add_tags', 'remove_tags', 'update_titles', 'update_snapshots', 'resnapshot_snapshot', 'overwrite_snapshots', 'delete_snapshots']
     actions = ['add_tags', 'remove_tags', 'update_titles', 'update_snapshots', 'resnapshot_snapshot', 'overwrite_snapshots', 'delete_snapshots']
     inlines = [TagInline, ArchiveResultInline]
     inlines = [TagInline, ArchiveResultInline]

+ 4 - 9
archivebox/core/models.py

@@ -138,16 +138,16 @@ class Snapshot(ABIDModel):
     abid_subtype_src = '"01"'
     abid_subtype_src = '"01"'
     abid_rand_src = 'self.old_id'
     abid_rand_src = 'self.old_id'
 
 
-    old_id = models.UUIDField(default=uuid.uuid4, editable=False, unique=True)  # legacy pk
-    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=True, unique=True)
+    old_id = models.UUIDField(default=None, null=False, editable=False, unique=True)  # legacy pk
+    id = models.UUIDField(default=None, null=False, primary_key=True, editable=True, unique=True)
     abid = ABIDField(prefix=abid_prefix)
     abid = ABIDField(prefix=abid_prefix)
 
 
     created_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, default=get_or_create_system_user_pk, related_name='snapshot_set')
     created_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, default=get_or_create_system_user_pk, related_name='snapshot_set')
-    created = AutoDateTimeField(default=timezone.now, db_index=True)
+    created = AutoDateTimeField(default=None, null=False, db_index=True)
     modified = models.DateTimeField(auto_now=True)
     modified = models.DateTimeField(auto_now=True)
 
 
     # legacy ts fields
     # legacy ts fields
-    added = AutoDateTimeField(default=timezone.now, db_index=True)
+    added = AutoDateTimeField(default=None, null=False, editable=True, db_index=True)
     updated = models.DateTimeField(auto_now=True, blank=True, null=True, db_index=True)
     updated = models.DateTimeField(auto_now=True, blank=True, null=True, db_index=True)
 
 
     url = models.URLField(unique=True, db_index=True)
     url = models.URLField(unique=True, db_index=True)
@@ -161,11 +161,6 @@ class Snapshot(ABIDModel):
 
 
     objects = SnapshotManager()
     objects = SnapshotManager()
 
 
-    def save(self, *args, **kwargs):
-        # make sure self.added is seeded with a value before calculating ABID using it
-        if self._state.adding or not self.added:
-            self.added = self.added or timezone.now()
-        return super().save(*args, **kwargs)
 
 
     def __repr__(self) -> str:
     def __repr__(self) -> str:
         title = (self.title_stripped or '-')[:64]
         title = (self.title_stripped or '-')[:64]