Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/multiple folder support #206

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 12 additions & 8 deletions .github/workflows/merge.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,12 @@ name: Merge Check

on:
pull_request_review:
# Trigger this workflow when a pull request review is submitted
# Trigger this workflow when a pull request review is submitted
types: [submitted]
pull_request:
paths:
- "backend/**"
- "frontend/**"

jobs:
build-tauri:
Expand All @@ -27,22 +31,22 @@ jobs:
uses: actions/setup-node@v4
with:
node-version: lts/*
cache: 'npm'
cache: "npm"

- name: Install Rust
uses: dtolnay/rust-toolchain@stable

- name: Rust cache
uses: swatinem/rust-cache@v2
with:
# Cache Rust build artifacts for faster build times
workspaces: './src-tauri -> target'
# Cache Rust build artifacts for faster build times
workspaces: "./src-tauri -> target"

- name: Install frontend dependencies
run: |
cd frontend
npm install

- name: Build Tauri
uses: tauri-apps/tauri-action@v0
with:
Expand All @@ -51,4 +55,4 @@ jobs:
env:
# Use secrets for signing the Tauri app
TAURI_SIGNING_PRIVATE_KEY: dW50cnVzdGVkIGNvbW1lbnQ6IHJzaWduIGVuY3J5cHRlZCBzZWNyZXQga2V5ClJXUlRZMEl5NlF2SjE3cWNXOVlQQ0JBTlNITEpOUVoyQ3ZuNTdOSkwyNE1NN2RmVWQ1a0FBQkFBQUFBQUFBQUFBQUlBQUFBQU9XOGpTSFNRd0Q4SjNSbm5Oc1E0OThIUGx6SS9lWXI3ZjJxN3BESEh1QTRiQXlkR2E5aG1oK1g0Tk5kcmFzc0IvZFZScEpubnptRkxlbDlUR2R1d1Y5OGRSYUVmUGoxNTFBcHpQZ1dSS2lHWklZVHNkV1Byd1VQSnZCdTZFWlVGOUFNVENBRlgweUU9Cg==
TAURI_SIGNING_PRIVATE_KEY_PASSWORD : pass
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: pass
202 changes: 139 additions & 63 deletions backend/app/routes/images.py
Original file line number Diff line number Diff line change
Expand Up @@ -251,41 +251,31 @@ def delete_multiple_images(payload: dict):
},
)
path = os.path.normpath(path)
parts = path.split(os.sep)
if "images" in parts:
parts.insert(parts.index("images") + 1, "PictoPy.thumbnails")
thumb_nail_image_path = os.sep.join(parts)

if os.path.exists(thumb_nail_image_path):
print(f"Thumbnail found: {thumb_nail_image_path}")
else:
print(f"Thumbnail not found: {thumb_nail_image_path}")
else:
thumb_nail_image_path = ""
print(f"'images' directory not found in path: {path}")
folder_path, filename = os.path.split(path)
thumbnail_folder = os.path.join(folder_path, "PictoPy.thumbnails")
thumb_nail_image_path = os.path.join(thumbnail_folder, filename)


if os.path.exists(path) :
try :
# Check and remove the original file
if os.path.exists(path):
try:
os.remove(path)
except PermissionError:
print(f"Permission denied for file '{thumb_nail_image_path}'.")
print(f"Permission denied for file '{path}'.")
except Exception as e:
print(f"An error occurred: {e}")

else:
print(f"File '{path}' does not exist.")

if os.path.exists(thumb_nail_image_path) :
try :

# Check and remove the thumbnail file
if os.path.exists(thumb_nail_image_path):
try:
os.remove(thumb_nail_image_path)
except PermissionError:
print(f"Permission denied for file '{thumb_nail_image_path}'.")
except Exception as e:
print(f"An error occurred: {e}")
else :
else:
print(f"File '{thumb_nail_image_path}' does not exist.")


delete_image_db(path)
deleted_paths.append(path)
Expand Down Expand Up @@ -489,6 +479,98 @@ async def add_folder(payload: dict):
@router.post("/generate-thumbnails")
@exception_handler_wrapper
def generate_thumbnails(payload: dict):
if "folder_paths" not in payload or not isinstance(payload["folder_paths"], list):
return JSONResponse(
status_code=400,
content={
"status_code": 400,
"content": {
"success": False,
"error": "Invalid or missing 'folder_paths' in payload",
"message": "'folder_paths' must be a list of folder paths",
},
},
)

folder_paths = payload["folder_paths"]
image_extensions = [".jpg", ".jpeg", ".png", ".bmp", ".gif", ".webp"]
failed_paths = []

for folder_path in folder_paths:
if not os.path.isdir(folder_path):
failed_paths.append(
{
"folder_path": folder_path,
"error": "Invalid folder path",
"message": "The provided path is not a valid directory",
}
)
continue

for root, _, files in os.walk(folder_path):
# Do not generate thumbnails for the "PictoPy.thumbnails" folder
if "PictoPy.thumbnails" in root:
continue

# Create the "PictoPy.thumbnails" folder in the current directory (`root`)
thumbnail_folder = os.path.join(root, "PictoPy.thumbnails")
os.makedirs(thumbnail_folder, exist_ok=True)

for file in files:
file_path = os.path.join(root, file)
file_extension = os.path.splitext(file_path)[1].lower()
if file_extension in image_extensions:
try:
# Create a unique thumbnail name based on the file name
thumbnail_name = file
thumbnail_path = os.path.join(thumbnail_folder, thumbnail_name)

# Skip if the thumbnail already exists
if os.path.exists(thumbnail_path):
continue

# Generate the thumbnail
img = Image.open(file_path)
img.thumbnail((400, 400))
img.save(thumbnail_path)
except Exception as e:
failed_paths.append(
{
"folder_path": folder_path,
"file": file_path,
"error": "Thumbnail generation error",
"message": f"Error processing file {file}: {str(e)}",
}
)

if failed_paths:
return JSONResponse(
status_code=207, # Multi-Status (some succeeded, some failed)
content={
"status_code": 207,
"content": {
"success": False,
"error": "Partial processing",
"message": "Some folders or files could not be processed",
"failed_paths": failed_paths,
},
},
)

return JSONResponse(
status_code=201,
content={
"data": "",
"message": "Thumbnails generated successfully for all valid folders",
"success": True,
},
)


# Delete all the thumbnails present in the given folder
@router.delete("/delete-thumbnails")
@exception_handler_wrapper
def delete_thumbnails(payload: dict):
if "folder_path" not in payload:
return JSONResponse(
status_code=400,
Expand Down Expand Up @@ -516,53 +598,47 @@ def generate_thumbnails(payload: dict):
},
)

thumbnail_folder = os.path.join(folder_path, "PictoPy.thumbnails")
os.makedirs(thumbnail_folder, exist_ok=True)
# List to store any errors encountered while deleting thumbnails
failed_deletions = []

image_extensions = [".jpg", ".jpeg", ".png", ".bmp", ".gif", ".webp"]
for root, _, files in os.walk(folder_path):
if "PictoPy.thumbnails" in root:
continue
for file in files:
file_path = os.path.join(root, file)
file_extension = os.path.splitext(file_path)[1].lower()
if file_extension in image_extensions:
# Walk through the folder path and find all `PictoPy.thumbnails` folders
for root, dirs, _ in os.walk(folder_path):
for dir_name in dirs:
if dir_name == "PictoPy.thumbnails":
thumbnail_folder = os.path.join(root, dir_name)
try:
# Create a unique name based on the relative folder structure
relative_path = os.path.relpath(root, folder_path)
sanitized_relative_path = relative_path.replace(
os.sep, "."
) # Replace path separators
thumbnail_name = (
f"{sanitized_relative_path}.{file}"
if relative_path != "."
else file
)
thumbnail_path = os.path.join(thumbnail_folder, thumbnail_name)
if os.path.exists(thumbnail_path):
# print(f"Thumbnail {thumbnail_name} already exists. Skipping.")
continue
img = Image.open(file_path)
img.thumbnail((400, 400))
img.save(thumbnail_path)
# Delete the thumbnail folder
shutil.rmtree(thumbnail_folder)
print(f"Deleted: {thumbnail_folder}")
except Exception as e:
return JSONResponse(
status_code=500,
content={
"status_code": 500,
"content": {
"success": False,
"error": "Internal server error",
"message": f"Error processing file {file}: {str(e)}",
},
},
failed_deletions.append(
{
"folder": thumbnail_folder,
"error": str(e),
}
)

if failed_deletions:
return JSONResponse(
status_code=500,
content={
"status_code": 500,
"content": {
"success": False,
"error": "Some thumbnail folders could not be deleted",
"message": "See the `failed_deletions` field for details.",
"failed_deletions": failed_deletions,
},
},
)

return JSONResponse(
status_code=201,
status_code=200,
content={
"data": "",
"message": "Thumbnails generated successfully",
"success": True,
"status_code": 200,
"content": {
"success": True,
"message": "All PictoPy.thumbnails folders have been successfully deleted.",
},
},
)
1 change: 1 addition & 0 deletions frontend/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"extends": ["react-app", "eslint-config-prettier"],
"ignorePatterns": ["src-tauri/target"],
"rules": {
// Accessibility rules
"jsx-a11y/click-events-have-key-events": "off",
Expand Down
18 changes: 14 additions & 4 deletions frontend/api/api-functions/images.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,7 @@ const parseAndSortImageData = (data: APIResponse['data']): Image[] => {
const parsedImages: Image[] = Object.entries(data.images).map(
([src, tags]) => {
const url = convertFileSrc(src);
const thumbnailUrl = convertFileSrc(
extractThumbnailPath(data.folder_path, src),
);
const thumbnailUrl = convertFileSrc(extractThumbnailPath(src));
return {
imagePath: src,
title: src.substring(src.lastIndexOf('\\') + 1),
Expand Down Expand Up @@ -86,12 +84,24 @@ export const addMultImages = async (paths: string[]) => {
return data;
};

export const generateThumbnails = async (folderPath: string) => {
export const generateThumbnails = async (folderPath: string[]) => {
const response = await fetch(imagesEndpoints.generateThumbnails, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ folder_paths: folderPath }),
});
const data = await response.json();
return data;
};

export const deleteThumbnails = async (folderPath: string) => {
const response = await fetch(imagesEndpoints.deleteThumbnails, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ folder_path: folderPath }),
});
const data = await response.json();
Expand Down
1 change: 1 addition & 0 deletions frontend/api/apiEndpoints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export const imagesEndpoints = {
addFolder: `${BACKED_URL}/images/add-folder`,
addMultipleImages: `${BACKED_URL}/images/multiple-images`,
generateThumbnails: `${BACKED_URL}/images/generate-thumbnails`,
deleteThumbnails: `${BACKED_URL}/images/delete-thumbnails`,
};

export const albumEndpoints = {
Expand Down
8 changes: 4 additions & 4 deletions frontend/babel.config.cjs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
module.exports = {
presets: [
["@babel/preset-env", { targets: { node: "current" } }],
"@babel/preset-typescript",
["@babel/preset-react", { runtime: "automatic" }],
['@babel/preset-env', { targets: { node: 'current' } }],
'@babel/preset-typescript',
['@babel/preset-react', { runtime: 'automatic' }],
],
}
};
1 change: 0 additions & 1 deletion frontend/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Tauri + React + Typescript</title>

</head>

<body>
Expand Down
Loading