diff --git a/src/tagstudio/core/library/alchemy/library.py b/src/tagstudio/core/library/alchemy/library.py index 473774b4..9797d6b8 100644 --- a/src/tagstudio/core/library/alchemy/library.py +++ b/src/tagstudio/core/library/alchemy/library.py @@ -94,6 +94,7 @@ from tagstudio.core.library.alchemy.models import ( from tagstudio.core.library.alchemy.visitors import SQLBoolExpressionBuilder from tagstudio.core.library.ignore import migrate_ext_list from tagstudio.core.library.json.library import Library as JsonLibrary +from tagstudio.core.utils.stat import get_date_created, get_date_modified from tagstudio.core.utils.types import unwrap if TYPE_CHECKING: @@ -650,6 +651,54 @@ class Library: make_transient(entry) return entry + def refresh_file_entry_stats(self, entry_id: int, path: Path | None): + """Updates a file entry's associated stat() data.""" + needs_update = False + + entry = self.get_entry_full(entry_id, with_fields=False, with_tags=False) + if not entry: + return + + if not path: + full_path = unwrap(self.library_dir) / entry.path + else: + full_path = unwrap(self.library_dir) / path + + file_date_created = get_date_created(full_path) + file_date_modified = get_date_modified(full_path) + + # Log info + if entry.date_created != file_date_created: + logger.info(full_path) + logger.warning(f"Difference in date_created!: {entry.date_created}/{file_date_created}") + needs_update = True + # else: + # logger.info("No difference in date_created.") + + if entry.date_modified != file_date_modified: + logger.info(full_path) + logger.warning( + f"Difference in date_modified!: {entry.date_modified}/{file_date_modified}" + ) + needs_update = True + # else: + # logger.info("No difference in date_modified") + + if not needs_update: + return + else: + logger.info(f"Updating entry file_metadata for {full_path}") + + with Session(self.engine) as session: + stmt = update(Entry).where(Entry.id == entry_id) + if file_date_created: + stmt = stmt.values(date_created=file_date_created) + if file_date_modified: + stmt = stmt.values(date_modified=file_date_modified) + + session.execute(stmt) + session.commit() + def get_tag_entries( self, tag_ids: Iterable[int], entry_ids: Iterable[int] ) -> dict[int, set[int]]: @@ -758,10 +807,10 @@ class Library: session.query(Entry).where(Entry.id.in_(sub_list)).delete() session.commit() - def has_entry_with_path(self, path: Path) -> bool: - """Check if an entry with this path is in the library.""" + def get_entry_id_from_path(self, path: Path) -> int: + """Attempt to return an Entry ID given a filepath, else return -1.""" with Session(self.engine) as session: - return session.query(exists().where(Entry.path == path)).scalar() + return session.scalar(select(Entry.id).where(Entry.path == path).limit(1)) or -1 def get_paths(self, limit: int = -1) -> list[str]: path_strings: list[str] = [] @@ -1079,7 +1128,7 @@ class Library: Returns True if the action succeeded and False if the path already exists. """ - if self.has_entry_with_path(path): + if self.get_entry_id_from_path(path): return False if isinstance(entry_id, Entry): entry_id = entry_id.id diff --git a/src/tagstudio/core/library/alchemy/models.py b/src/tagstudio/core/library/alchemy/models.py index 4daf42d5..b357cd45 100644 --- a/src/tagstudio/core/library/alchemy/models.py +++ b/src/tagstudio/core/library/alchemy/models.py @@ -202,8 +202,8 @@ class Entry(Base): path: Mapped[Path] = mapped_column(PathType, unique=True) filename: Mapped[str] = mapped_column() suffix: Mapped[str] = mapped_column() - date_created: Mapped[dt | None] - date_modified: Mapped[dt | None] + date_created: Mapped[float | None] + date_modified: Mapped[float | None] date_added: Mapped[dt | None] tags: Mapped[set[Tag]] = relationship(secondary="tag_entries") @@ -237,8 +237,8 @@ class Entry(Base): path: Path, fields: list[BaseField], id: int | None = None, - date_created: dt | None = None, - date_modified: dt | None = None, + date_created: float | None = None, + date_modified: float | None = None, date_added: dt | None = None, ) -> None: super().__init__() diff --git a/src/tagstudio/core/library/refresh.py b/src/tagstudio/core/library/refresh.py index 18848366..a14469c4 100644 --- a/src/tagstudio/core/library/refresh.py +++ b/src/tagstudio/core/library/refresh.py @@ -16,6 +16,8 @@ from tagstudio.core.library.alchemy.library import Library from tagstudio.core.library.alchemy.models import Entry from tagstudio.core.library.ignore import PATH_GLOB_FLAGS, Ignore, ignore_to_glob from tagstudio.core.utils.silent_subprocess import silent_run # pyright: ignore +from tagstudio.core.utils.stat import get_date_created, get_date_modified +from tagstudio.core.utils.types import unwrap logger = structlog.get_logger(__name__) @@ -41,6 +43,8 @@ class RefreshTracker: Entry( path=entry_path, fields=[], + date_created=get_date_created(unwrap(self.library.library_dir) / entry_path), + date_modified=get_date_modified(unwrap(self.library.library_dir) / entry_path), date_added=dt.now(), ) for entry_path in self.files_not_in_library[index:end] @@ -140,6 +144,10 @@ class RefreshTracker: # Skip if the file/path is already mapped in the Library if f in self.library.included_files: dir_file_count += 1 + + entry_id = self.library.get_entry_id_from_path(f) + self.library.refresh_file_entry_stats(entry_id, path=f) + continue # Ignore if the file is a directory @@ -149,8 +157,13 @@ class RefreshTracker: dir_file_count += 1 self.library.included_files.add(f) - if not self.library.has_entry_with_path(f): + # if not self.library.has_entry_with_path(f): + # self.files_not_in_library.append(f) + entry_id = self.library.get_entry_id_from_path(f) + if entry_id < 0: self.files_not_in_library.append(f) + else: + self.library.refresh_file_entry_stats(entry_id, path=f) end_time_total = time() yield dir_file_count @@ -183,6 +196,9 @@ class RefreshTracker: # Skip if the file/path is already mapped in the Library if f in self.library.included_files: dir_file_count += 1 + relative_path = f.relative_to(library_dir) + entry_id = self.library.get_entry_id_from_path(relative_path) + self.library.refresh_file_entry_stats(entry_id, path=relative_path) continue # Ignore if the file is a directory @@ -194,8 +210,14 @@ class RefreshTracker: relative_path = f.relative_to(library_dir) - if not self.library.has_entry_with_path(relative_path): + # if not self.library.has_entry_with_path(relative_path): + # self.files_not_in_library.append(relative_path) + entry_id = self.library.get_entry_id_from_path(relative_path) + if entry_id < 0: self.files_not_in_library.append(relative_path) + else: + self.library.refresh_file_entry_stats(entry_id, path=relative_path) + except ValueError: logger.info("[Refresh]: ValueError when refreshing directory with wcmatch!") diff --git a/src/tagstudio/core/utils/stat.py b/src/tagstudio/core/utils/stat.py new file mode 100644 index 00000000..7ee46300 --- /dev/null +++ b/src/tagstudio/core/utils/stat.py @@ -0,0 +1,16 @@ +# SPDX-FileCopyrightText: (c) TagStudio Contributors +# SPDX-License-Identifier: MIT + +import platform +from pathlib import Path + + +def get_date_modified(path: Path) -> float: + return path.stat().st_mtime + + +def get_date_created(path: Path) -> float: + if platform.system() in {"Windows", "Darwin"}: + return path.stat().st_birthtime + else: + return path.stat().st_ctime diff --git a/tests/core/library/test_library.py b/tests/core/library/test_library.py index e260225a..2ee25f64 100644 --- a/tests/core/library/test_library.py +++ b/tests/core/library/test_library.py @@ -80,9 +80,9 @@ def test_library_add_file(library: Library): fields=[TextField(name="Title", value="I'm a Test Title")], ) - assert not library.has_entry_with_path(entry.path) + assert not library.get_entry_id_from_path(entry.path) assert library.add_entries([entry]) - assert library.has_entry_with_path(entry.path) + assert library.get_entry_id_from_path(entry.path) def test_create_tag(library: Library, generate_tag: Callable[..., Tag]): @@ -338,8 +338,8 @@ def test_merge_entries(library: Library): entry_b_: Entry = unwrap(library.get_entry_full(entry_b_id)) assert library.merge_entries(entry_a_, entry_b_) - assert not library.has_entry_with_path(Path("a")) - assert library.has_entry_with_path(Path("b")) + assert not library.get_entry_id_from_path(Path("a")) + assert library.get_entry_id_from_path(Path("b")) entry_b_merged = unwrap(library.get_entry_full(entry_b_id))