Skip to content

Commit fc2021c

Browse files
Merge pull request #486 from rahulharpal1603/backend-revamp
Add sync microservice for watching file system events
2 parents 1fd1cb0 + ba8cf74 commit fc2021c

File tree

27 files changed

+1601
-128
lines changed

27 files changed

+1601
-128
lines changed

backend/app/database/folders.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -342,6 +342,46 @@ def db_get_folder_ids_by_path_prefix(root_path: str) -> List[FolderIdPath]:
342342
conn.close()
343343

344344

345+
def db_get_folder_ids_by_paths(
346+
folder_paths: List[FolderPath],
347+
) -> Dict[FolderPath, FolderId]:
348+
"""
349+
Get folder IDs for multiple folder paths in a single database query.
350+
351+
Args:
352+
folder_paths: List of folder paths to look up
353+
354+
Returns:
355+
Dictionary mapping folder paths to their corresponding folder IDs
356+
"""
357+
if not folder_paths:
358+
return {}
359+
360+
conn = sqlite3.connect(DATABASE_PATH)
361+
cursor = conn.cursor()
362+
363+
try:
364+
# Convert all paths to absolute paths
365+
abs_paths = [os.path.abspath(path) for path in folder_paths]
366+
367+
# Create placeholders for the IN clause
368+
placeholders = ",".join("?" * len(abs_paths))
369+
370+
cursor.execute(
371+
f"SELECT folder_path, folder_id FROM folders WHERE folder_path IN ({placeholders})",
372+
abs_paths,
373+
)
374+
375+
results = cursor.fetchall()
376+
377+
# Create a mapping from folder_path to folder_id
378+
path_to_id = {folder_path: folder_id for folder_path, folder_id in results}
379+
380+
return path_to_id
381+
finally:
382+
conn.close()
383+
384+
345385
def db_get_direct_child_folders(parent_folder_id: str) -> List[Tuple[str, str]]:
346386
"""
347387
Get all direct child folders (not subfolders) for a given parent folder.

backend/app/database/images.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,3 +195,75 @@ def db_insert_image_classes_batch(image_class_pairs: List[ImageClassPair]) -> bo
195195
return False
196196
finally:
197197
conn.close()
198+
199+
200+
def db_get_images_by_folder_ids(
201+
folder_ids: List[int],
202+
) -> List[Tuple[ImageId, ImagePath, str]]:
203+
"""
204+
Get all images that belong to the specified folder IDs.
205+
206+
Args:
207+
folder_ids: List of folder IDs to search for images
208+
209+
Returns:
210+
List of tuples containing (image_id, image_path, thumbnail_path)
211+
"""
212+
if not folder_ids:
213+
return []
214+
215+
conn = sqlite3.connect(DATABASE_PATH)
216+
cursor = conn.cursor()
217+
218+
try:
219+
# Create placeholders for the IN clause
220+
placeholders = ",".join("?" for _ in folder_ids)
221+
cursor.execute(
222+
f"""
223+
SELECT id, path, thumbnailPath
224+
FROM images
225+
WHERE folder_id IN ({placeholders})
226+
""",
227+
folder_ids,
228+
)
229+
return cursor.fetchall()
230+
except Exception as e:
231+
print(f"Error getting images by folder IDs: {e}")
232+
return []
233+
finally:
234+
conn.close()
235+
236+
237+
def db_delete_images_by_ids(image_ids: List[ImageId]) -> bool:
238+
"""
239+
Delete multiple images from the database by their IDs.
240+
This will also delete associated records in image_classes due to CASCADE.
241+
242+
Args:
243+
image_ids: List of image IDs to delete
244+
245+
Returns:
246+
True if deletion was successful, False otherwise
247+
"""
248+
if not image_ids:
249+
return True
250+
251+
conn = sqlite3.connect(DATABASE_PATH)
252+
cursor = conn.cursor()
253+
254+
try:
255+
# Create placeholders for the IN clause
256+
placeholders = ",".join("?" for _ in image_ids)
257+
cursor.execute(
258+
f"DELETE FROM images WHERE id IN ({placeholders})",
259+
image_ids,
260+
)
261+
conn.commit()
262+
print(f"Deleted {cursor.rowcount} obsolete image(s) from database")
263+
return True
264+
except Exception as e:
265+
print(f"Error deleting images: {e}")
266+
conn.rollback()
267+
return False
268+
finally:
269+
conn.close()

backend/app/routes/folders.py

Lines changed: 61 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from fastapi import APIRouter, HTTPException, status, Depends, Request
2+
from typing import List, Tuple
23
from app.database.folders import (
34
db_update_parent_ids_for_subtree,
45
db_folder_exists,
@@ -7,6 +8,7 @@
78
db_disable_ai_tagging_batch,
89
db_delete_folders_batch,
910
db_get_direct_child_folders,
11+
db_get_folder_ids_by_path_prefix,
1012
)
1113
from app.schemas.folders import (
1214
AddFolderRequest,
@@ -37,15 +39,24 @@
3739
router = APIRouter()
3840

3941

40-
def post_folder_add_sequence(folder_path: str):
42+
def post_folder_add_sequence(folder_path: str, folder_id: int):
4143
"""
4244
Post-addition sequence for a folder.
4345
This function is called after a folder is successfully added.
4446
It processes images in the folder and updates the database.
4547
"""
4648
try:
47-
# Process images in the folder
48-
image_util_process_folder_images(folder_path)
49+
# Get all folder IDs and paths that match the root path prefix
50+
folder_data = []
51+
folder_ids_and_paths = db_get_folder_ids_by_path_prefix(folder_path)
52+
53+
# Set all folders to non-recursive (False)
54+
for folder_id_from_db, folder_path_from_db in folder_ids_and_paths:
55+
folder_data.append((folder_path_from_db, folder_id_from_db, False))
56+
57+
print("Add folder: ", folder_data)
58+
# Process images in all folders
59+
image_util_process_folder_images(folder_data)
4960

5061
except Exception as e:
5162
print(f"Error in post processing after folder {folder_path} was added: {e}")
@@ -68,6 +79,34 @@ def post_AI_tagging_enabled_sequence():
6879
return True
6980

7081

82+
def post_sync_folder_sequence(
83+
folder_path: str, folder_id: int, added_folders: List[Tuple[str, str]]
84+
):
85+
"""
86+
Post-sync sequence for a folder.
87+
This function is called after a folder is synced.
88+
It processes images in the folder and updates the database.
89+
"""
90+
try:
91+
# Create folder data array
92+
folder_data = []
93+
94+
folder_data.append((folder_path, folder_id, False))
95+
96+
for added_folder_id, added_folder_path in added_folders:
97+
folder_data.append((added_folder_path, added_folder_id, False))
98+
99+
print("Sync folder: ", folder_data)
100+
# Process images in all folders
101+
image_util_process_folder_images(folder_data)
102+
image_util_process_untagged_images()
103+
cluster_util_face_clusters_sync()
104+
except Exception as e:
105+
print(f"Error in post processing after folder {folder_path} was synced: {e}")
106+
return False
107+
return True
108+
109+
71110
def get_state(request: Request):
72111
return request.app.state
73112

@@ -132,7 +171,7 @@ def add_folder(request: AddFolderRequest, app_state=Depends(get_state)):
132171

133172
# Step 6: Call the post-addition sequence in a separate process
134173
executor: ProcessPoolExecutor = app_state.executor
135-
executor.submit(post_folder_add_sequence, request.folder_path)
174+
executor.submit(post_folder_add_sequence, request.folder_path, root_folder_id)
136175

137176
return AddFolderResponse(
138177
success=True,
@@ -287,33 +326,43 @@ def delete_folders(request: DeleteFoldersRequest):
287326
response_model=SyncFolderResponse,
288327
responses={code: {"model": ErrorResponse} for code in [400, 404, 500]},
289328
)
290-
def sync_folder(request: SyncFolderRequest):
329+
def sync_folder(request: SyncFolderRequest, app_state=Depends(get_state)):
291330
"""Sync a folder by comparing filesystem folders with database entries and removing extra DB entries."""
292331
try:
293-
# Step 1: Validate request
294-
295-
# Step 2: Get current state from both sources
332+
# Step 1: Get current state from both sources
296333
db_child_folders = db_get_direct_child_folders(request.folder_id)
297334
filesystem_folders = folder_util_get_filesystem_direct_child_folders(
298335
request.folder_path
299336
)
300337

301-
# Step 3: Compare and identify differences
338+
# Step 2: Compare and identify differences
302339
filesystem_folder_set = set(filesystem_folders)
303340
db_folder_paths = {folder_path for folder_id, folder_path in db_child_folders}
304341

305342
folders_to_delete = db_folder_paths - filesystem_folder_set
306343
folders_to_add = filesystem_folder_set - db_folder_paths
307344

308-
# Step 4: Perform synchronization operations
345+
# Step 3: Perform synchronization operations
309346
deleted_count, deleted_folders = folder_util_delete_obsolete_folders(
310347
db_child_folders, folders_to_delete
311348
)
312-
added_count, added_folders = folder_util_add_multiple_folder_trees(
349+
added_count, added_folders_with_ids = folder_util_add_multiple_folder_trees(
313350
folders_to_add, request.folder_id
314351
)
315352

316-
# Step 5: Return comprehensive response
353+
# Extract just the paths for the API response
354+
added_folders = [
355+
folder_path for folder_id, folder_path in added_folders_with_ids
356+
]
357+
358+
executor: ProcessPoolExecutor = app_state.executor
359+
executor.submit(
360+
post_sync_folder_sequence,
361+
request.folder_path,
362+
request.folder_id,
363+
added_folders_with_ids,
364+
)
365+
# Step 4: Return comprehensive response
317366
return SyncFolderResponse(
318367
success=True,
319368
message=f"Successfully synced folder. Added {added_count} folder(s), deleted {deleted_count} folder(s)",

backend/app/utils/folders.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ def folder_util_delete_obsolete_folders(
131131

132132
def folder_util_add_multiple_folder_trees(
133133
folders_to_add: set, parent_folder_id: str
134-
) -> Tuple[int, List[str]]:
134+
) -> Tuple[int, List[Tuple[str, str]]]:
135135
"""
136136
Add multiple folder trees with same parent to the database.
137137
@@ -140,12 +140,12 @@ def folder_util_add_multiple_folder_trees(
140140
parent_folder_id: ID of the parent folder
141141
142142
Returns:
143-
Tuple of (added_count, added_folders_list)
143+
Tuple of (added_count, added_folders_list) where added_folders_list contains (folder_id, folder_path) tuples
144144
"""
145145
if not folders_to_add:
146146
return 0, []
147147

148-
added_folders = []
148+
added_folders = [] # List of (folder_id, folder_path) tuples
149149
added_count = 0
150150

151151
for folder_path in folders_to_add:
@@ -161,7 +161,10 @@ def folder_util_add_multiple_folder_trees(
161161
# Update parent IDs for the new folder tree
162162
db_update_parent_ids_for_subtree(folder_path, folder_map)
163163

164-
added_folders.append(folder_path)
164+
# Add all folders from the folder_map as (folder_id, folder_path) tuples
165+
for folder_path_in_map, (folder_id_in_map, _) in folder_map.items():
166+
added_folders.append((folder_id_in_map, folder_path_in_map))
167+
165168
added_count += len(folder_map) # Count all folders in the tree
166169

167170
except Exception as e:

0 commit comments

Comments
 (0)