11import re
22import shutil
3+ import sys
34import unicodedata
45from dataclasses import dataclass
56from datetime import UTC , datetime
3435 TAG_ARCHIVED ,
3536 TAG_FAVORITE ,
3637 TS_FOLDER_NAME ,
37- LibraryPrefs ,
3838)
39+ from ...enums import LibraryPrefs
3940from .db import make_tables
4041from .enums import FieldTypeEnum , FilterState , TagColor
4142from .fields import (
4849from .joins import TagField , TagSubtag
4950from .models import Entry , Folder , Preferences , Tag , TagAlias , ValueType
5051
51- LIBRARY_FILENAME : str = "ts_library.sqlite"
52-
5352logger = structlog .get_logger (__name__ )
5453
5554
@@ -115,6 +114,15 @@ def __getitem__(self, index: int) -> Entry:
115114 return self .items [index ]
116115
117116
117+ @dataclass
118+ class LibraryStatus :
119+ """Keep status of library opening operation."""
120+
121+ success : bool
122+ library_path : Path | None = None
123+ message : str | None = None
124+
125+
118126class Library :
119127 """Class for the Library object, and all CRUD operations made upon it."""
120128
@@ -123,30 +131,28 @@ class Library:
123131 engine : Engine | None
124132 folder : Folder | None
125133
134+ FILENAME : str = "ts_library.sqlite"
135+
126136 def close (self ):
127137 if self .engine :
128138 self .engine .dispose ()
129139 self .library_dir = None
130140 self .storage_path = None
131141 self .folder = None
132142
133- def open_library (self , library_dir : Path | str , storage_path : str | None = None ) -> None :
134- if isinstance (library_dir , str ):
135- library_dir = Path (library_dir )
136-
137- self .library_dir = library_dir
143+ def open_library (self , library_dir : Path , storage_path : str | None = None ) -> LibraryStatus :
138144 if storage_path == ":memory:" :
139145 self .storage_path = storage_path
140146 else :
141- self .verify_ts_folders (self . library_dir )
142- self .storage_path = self . library_dir / TS_FOLDER_NAME / LIBRARY_FILENAME
147+ self .verify_ts_folders (library_dir )
148+ self .storage_path = library_dir / TS_FOLDER_NAME / self . FILENAME
143149
144150 connection_string = URL .create (
145151 drivername = "sqlite" ,
146152 database = str (self .storage_path ),
147153 )
148154
149- logger .info ("opening library" , connection_string = connection_string )
155+ logger .info ("opening library" , library_dir = library_dir , connection_string = connection_string )
150156 self .engine = create_engine (connection_string )
151157 with Session (self .engine ) as session :
152158 make_tables (self .engine )
@@ -159,9 +165,24 @@ def open_library(self, library_dir: Path | str, storage_path: str | None = None)
159165 # default tags may exist already
160166 session .rollback ()
161167
168+ if "pytest" not in sys .modules :
169+ db_version = session .scalar (
170+ select (Preferences ).where (Preferences .key == LibraryPrefs .DB_VERSION .name )
171+ )
172+
173+ if not db_version :
174+ # TODO - remove after #503 is merged and LibraryPrefs.DB_VERSION increased again
175+ return LibraryStatus (
176+ success = False ,
177+ message = (
178+ "Library version mismatch.\n "
179+ f"Found: v0, expected: v{ LibraryPrefs .DB_VERSION .default } "
180+ ),
181+ )
182+
162183 for pref in LibraryPrefs :
163184 try :
164- session .add (Preferences (key = pref .name , value = pref .value ))
185+ session .add (Preferences (key = pref .name , value = pref .default ))
165186 session .commit ()
166187 except IntegrityError :
167188 logger .debug ("preference already exists" , pref = pref )
@@ -183,11 +204,30 @@ def open_library(self, library_dir: Path | str, storage_path: str | None = None)
183204 logger .debug ("ValueType already exists" , field = field )
184205 session .rollback ()
185206
207+ db_version = session .scalar (
208+ select (Preferences ).where (Preferences .key == LibraryPrefs .DB_VERSION .name )
209+ )
210+ # if the db version is different, we cant proceed
211+ if db_version .value != LibraryPrefs .DB_VERSION .default :
212+ logger .error (
213+ "DB version mismatch" ,
214+ db_version = db_version .value ,
215+ expected = LibraryPrefs .DB_VERSION .default ,
216+ )
217+ # TODO - handle migration
218+ return LibraryStatus (
219+ success = False ,
220+ message = (
221+ "Library version mismatch.\n "
222+ f"Found: v{ db_version .value } , expected: v{ LibraryPrefs .DB_VERSION .default } "
223+ ),
224+ )
225+
186226 # check if folder matching current path exists already
187- self .folder = session .scalar (select (Folder ).where (Folder .path == self . library_dir ))
227+ self .folder = session .scalar (select (Folder ).where (Folder .path == library_dir ))
188228 if not self .folder :
189229 folder = Folder (
190- path = self . library_dir ,
230+ path = library_dir ,
191231 uuid = str (uuid4 ()),
192232 )
193233 session .add (folder )
@@ -196,6 +236,10 @@ def open_library(self, library_dir: Path | str, storage_path: str | None = None)
196236 session .commit ()
197237 self .folder = folder
198238
239+ # everything is fine, set the library path
240+ self .library_dir = library_dir
241+ return LibraryStatus (success = True , library_path = library_dir )
242+
199243 @property
200244 def default_fields (self ) -> list [BaseField ]:
201245 with Session (self .engine ) as session :
@@ -324,15 +368,18 @@ def add_entries(self, items: list[Entry]) -> list[int]:
324368
325369 with Session (self .engine ) as session :
326370 # add all items
327- session .add_all (items )
328- session .flush ()
329371
330- new_ids = [item .id for item in items ]
372+ try :
373+ session .add_all (items )
374+ session .commit ()
375+ except IntegrityError :
376+ session .rollback ()
377+ logger .exception ("IntegrityError" )
378+ return []
331379
380+ new_ids = [item .id for item in items ]
332381 session .expunge_all ()
333382
334- session .commit ()
335-
336383 return new_ids
337384
338385 def remove_entries (self , entry_ids : list [int ]) -> None :
@@ -396,9 +443,9 @@ def search_library(
396443
397444 if not search .id : # if `id` is set, we don't need to filter by extensions
398445 if extensions and is_exclude_list :
399- statement = statement .where (Entry .path . notilike ( f"%. { ',' . join ( extensions ) } " ))
446+ statement = statement .where (Entry .suffix . notin_ ( extensions ))
400447 elif extensions :
401- statement = statement .where (Entry .path . ilike ( f"%. { ',' . join ( extensions ) } " ))
448+ statement = statement .where (Entry .suffix . in_ ( extensions ))
402449
403450 statement = statement .options (
404451 selectinload (Entry .text_fields ),
@@ -770,7 +817,7 @@ def save_library_backup_to_disk(self) -> Path:
770817 target_path = self .library_dir / TS_FOLDER_NAME / BACKUP_FOLDER_NAME / filename
771818
772819 shutil .copy2 (
773- self .library_dir / TS_FOLDER_NAME / LIBRARY_FILENAME ,
820+ self .library_dir / TS_FOLDER_NAME / self . FILENAME ,
774821 target_path ,
775822 )
776823
0 commit comments