feat: store and update file created and modified dates

This commit is contained in:
Travis Abendshien
2026-09-06 12:12:29 -07:00
parent 658b7840c4
commit bf1018cd7e
5 changed files with 101 additions and 14 deletions
+53 -4
View File
@@ -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
+4 -4
View File
@@ -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__()
+24 -2
View File
@@ -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!")
+16
View File
@@ -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
+4 -4
View File
@@ -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))