mirror of
https://github.com/kyantech/Palmr.git
synced 2026-01-09 06:02:28 +08:00
[RELEASE] v3.3.2-beta (#372)
This commit is contained in:
commit
5cdc8c41e4
67
Dockerfile
67
Dockerfile
@ -163,11 +163,11 @@ mkdir -p /app/server/uploads /app/server/temp-uploads /app/server/prisma /app/se
|
||||
# This runs on EVERY startup to handle updates and corrupted metadata
|
||||
echo "🔐 Fixing permissions for internal storage..."
|
||||
|
||||
# DYNAMIC: Detect palmr user's actual UID and GID
|
||||
# Works with any Docker --user configuration
|
||||
PALMR_UID=\$(id -u palmr 2>/dev/null || echo "1001")
|
||||
PALMR_GID=\$(id -g palmr 2>/dev/null || echo "1001")
|
||||
echo " Target user: palmr (UID:\$PALMR_UID, GID:\$PALMR_GID)"
|
||||
# USE ENVIRONMENT VARIABLES: Allow runtime UID/GID configuration
|
||||
# Falls back to palmr user's UID/GID if not specified
|
||||
TARGET_UID=\${PALMR_UID:-\$(id -u palmr 2>/dev/null || echo "1001")}
|
||||
TARGET_GID=\${PALMR_GID:-\$(id -g palmr 2>/dev/null || echo "1001")}
|
||||
echo " Target user: palmr (UID:\$TARGET_UID, GID:\$TARGET_GID)"
|
||||
|
||||
# ALWAYS remove storage system metadata to prevent corruption issues
|
||||
# This is safe - storage system recreates it automatically
|
||||
@ -177,10 +177,59 @@ if [ -d "/app/server/minio-data/.minio.sys" ]; then
|
||||
rm -rf /app/server/minio-data/.minio.sys 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# Fix ownership and permissions (safe for updates)
|
||||
echo " 🔧 Setting ownership and permissions..."
|
||||
chown -R \$PALMR_UID:\$PALMR_GID /app/server 2>/dev/null || echo " ⚠️ chown skipped"
|
||||
chmod -R 755 /app/server 2>/dev/null || echo " ⚠️ chmod skipped"
|
||||
# SMART CHOWN: Only run expensive recursive chown when UID/GID changed
|
||||
# This dramatically speeds up subsequent starts
|
||||
UIDGID_MARKER="/app/server/.palmr-uidgid"
|
||||
CURRENT_OWNER="\$TARGET_UID:\$TARGET_GID"
|
||||
NEEDS_CHOWN=false
|
||||
|
||||
if [ -f "\$UIDGID_MARKER" ]; then
|
||||
STORED_OWNER=\$(cat "\$UIDGID_MARKER" 2>/dev/null || echo "")
|
||||
if [ "\$STORED_OWNER" != "\$CURRENT_OWNER" ]; then
|
||||
echo " 📝 UID/GID changed (\$STORED_OWNER → \$CURRENT_OWNER)"
|
||||
NEEDS_CHOWN=true
|
||||
else
|
||||
echo " ✓ UID/GID unchanged (\$CURRENT_OWNER), skipping chown"
|
||||
fi
|
||||
else
|
||||
echo " 📝 First run or marker missing, will set ownership"
|
||||
NEEDS_CHOWN=true
|
||||
fi
|
||||
|
||||
if [ "\$NEEDS_CHOWN" = "true" ]; then
|
||||
echo " 🔧 Setting ownership (this may take a moment on first run)..."
|
||||
|
||||
# Only chown the directories that need it
|
||||
chown \$TARGET_UID:\$TARGET_GID /app/server 2>/dev/null || true
|
||||
|
||||
# For most directories, just chown the directory itself (fast)
|
||||
for dir in uploads temp-uploads; do
|
||||
if [ -d "/app/server/\$dir" ]; then
|
||||
chown \$TARGET_UID:\$TARGET_GID "/app/server/\$dir" 2>/dev/null || true
|
||||
fi
|
||||
done
|
||||
|
||||
# For prisma directory, we need recursive chown for database files
|
||||
if [ -d "/app/server/prisma" ]; then
|
||||
echo " 🔧 Fixing database permissions..."
|
||||
chown -R \$TARGET_UID:\$TARGET_GID "/app/server/prisma" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# For minio-data, we NEED recursive chown because MinIO creates subdirectories
|
||||
# and needs write access to all of them
|
||||
if [ -d "/app/server/minio-data" ]; then
|
||||
echo " 🔧 Fixing MinIO storage permissions..."
|
||||
chown -R \$TARGET_UID:\$TARGET_GID "/app/server/minio-data" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# Save current UID/GID to marker
|
||||
echo "\$CURRENT_OWNER" > "\$UIDGID_MARKER"
|
||||
chown \$TARGET_UID:\$TARGET_GID "\$UIDGID_MARKER" 2>/dev/null || true
|
||||
|
||||
echo " ✅ Ownership updated and cached"
|
||||
fi
|
||||
|
||||
chmod 755 /app/server 2>/dev/null || echo " ⚠️ chmod skipped"
|
||||
|
||||
# Verify critical directories are writable
|
||||
if touch /app/server/.test-write 2>/dev/null; then
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "palmr-docs",
|
||||
"version": "3.3.1-beta",
|
||||
"version": "3.3.2-beta",
|
||||
"description": "Docs for Palmr",
|
||||
"private": true,
|
||||
"author": "Daniel Luiz Alves <daniel@kyantech.com.br>",
|
||||
@ -36,7 +36,7 @@
|
||||
"fumadocs-ui": "15.2.7",
|
||||
"lucide-react": "^0.525.0",
|
||||
"motion": "^12.23.0",
|
||||
"next": "15.3.4",
|
||||
"next": "15.3.6",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"tailwind-merge": "^3.2.0"
|
||||
@ -53,7 +53,7 @@
|
||||
"@typescript-eslint/eslint-plugin": "8.35.1",
|
||||
"@typescript-eslint/parser": "8.35.1",
|
||||
"eslint": "9.30.0",
|
||||
"eslint-config-next": "15.3.4",
|
||||
"eslint-config-next": "15.3.6",
|
||||
"eslint-config-prettier": "9.1.0",
|
||||
"eslint-plugin-prettier": "5.5.1",
|
||||
"postcss": "^8.5.6",
|
||||
|
||||
122
apps/docs/pnpm-lock.yaml
generated
122
apps/docs/pnpm-lock.yaml
generated
@ -19,13 +19,13 @@ importers:
|
||||
version: 2.1.1
|
||||
fumadocs-core:
|
||||
specifier: 15.2.7
|
||||
version: 15.2.7(@types/react@19.1.8)(next@15.3.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
version: 15.2.7(@types/react@19.1.8)(next@15.3.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
fumadocs-mdx:
|
||||
specifier: 11.6.10
|
||||
version: 11.6.10(acorn@8.15.0)(fumadocs-core@15.2.7(@types/react@19.1.8)(next@15.3.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(next@15.3.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0))
|
||||
version: 11.6.10(acorn@8.15.0)(fumadocs-core@15.2.7(@types/react@19.1.8)(next@15.3.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(next@15.3.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0))
|
||||
fumadocs-ui:
|
||||
specifier: 15.2.7
|
||||
version: 15.2.7(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(next@15.3.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(tailwindcss@4.1.11)
|
||||
version: 15.2.7(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(next@15.3.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(tailwindcss@4.1.11)
|
||||
lucide-react:
|
||||
specifier: ^0.525.0
|
||||
version: 0.525.0(react@19.1.0)
|
||||
@ -33,8 +33,8 @@ importers:
|
||||
specifier: ^12.23.0
|
||||
version: 12.23.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
next:
|
||||
specifier: 15.3.4
|
||||
version: 15.3.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
specifier: 15.3.6
|
||||
version: 15.3.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
react:
|
||||
specifier: ^19.1.0
|
||||
version: 19.1.0
|
||||
@ -79,8 +79,8 @@ importers:
|
||||
specifier: 9.30.0
|
||||
version: 9.30.0(jiti@2.4.2)
|
||||
eslint-config-next:
|
||||
specifier: 15.3.4
|
||||
version: 15.3.4(eslint@9.30.0(jiti@2.4.2))(typescript@5.8.3)
|
||||
specifier: 15.3.6
|
||||
version: 15.3.6(eslint@9.30.0(jiti@2.4.2))(typescript@5.8.3)
|
||||
eslint-config-prettier:
|
||||
specifier: 9.1.0
|
||||
version: 9.1.0(eslint@9.30.0(jiti@2.4.2))
|
||||
@ -540,56 +540,56 @@ packages:
|
||||
'@napi-rs/wasm-runtime@0.2.11':
|
||||
resolution: {integrity: sha512-9DPkXtvHydrcOsopiYpUgPHpmj0HWZKMUnL2dZqpvC42lsratuBG06V5ipyno0fUek5VlFsNQ+AcFATSrJXgMA==}
|
||||
|
||||
'@next/env@15.3.4':
|
||||
resolution: {integrity: sha512-ZkdYzBseS6UjYzz6ylVKPOK+//zLWvD6Ta+vpoye8cW11AjiQjGYVibF0xuvT4L0iJfAPfZLFidaEzAOywyOAQ==}
|
||||
'@next/env@15.3.6':
|
||||
resolution: {integrity: sha512-/cK+QPcfRbDZxmI/uckT4lu9pHCfRIPBLqy88MhE+7Vg5hKrEYc333Ae76dn/cw2FBP2bR/GoK/4DU+U7by/Nw==}
|
||||
|
||||
'@next/eslint-plugin-next@15.3.4':
|
||||
resolution: {integrity: sha512-lBxYdj7TI8phbJcLSAqDt57nIcobEign5NYIKCiy0hXQhrUbTqLqOaSDi568U6vFg4hJfBdZYsG4iP/uKhCqgg==}
|
||||
'@next/eslint-plugin-next@15.3.6':
|
||||
resolution: {integrity: sha512-gvt7l1r4N0zHCXyXYj39ObrTBr8TxyA/306Z/kjseYk6hiefu3zexRKRVjVmQqUpxe9oxyfYWMZFtsBYPgr1oA==}
|
||||
|
||||
'@next/swc-darwin-arm64@15.3.4':
|
||||
resolution: {integrity: sha512-z0qIYTONmPRbwHWvpyrFXJd5F9YWLCsw3Sjrzj2ZvMYy9NPQMPZ1NjOJh4ojr4oQzcGYwgJKfidzehaNa1BpEg==}
|
||||
'@next/swc-darwin-arm64@15.3.5':
|
||||
resolution: {integrity: sha512-lM/8tilIsqBq+2nq9kbTW19vfwFve0NR7MxfkuSUbRSgXlMQoJYg+31+++XwKVSXk4uT23G2eF/7BRIKdn8t8w==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@next/swc-darwin-x64@15.3.4':
|
||||
resolution: {integrity: sha512-Z0FYJM8lritw5Wq+vpHYuCIzIlEMjewG2aRkc3Hi2rcbULknYL/xqfpBL23jQnCSrDUGAo/AEv0Z+s2bff9Zkw==}
|
||||
'@next/swc-darwin-x64@15.3.5':
|
||||
resolution: {integrity: sha512-WhwegPQJ5IfoUNZUVsI9TRAlKpjGVK0tpJTL6KeiC4cux9774NYE9Wu/iCfIkL/5J8rPAkqZpG7n+EfiAfidXA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@next/swc-linux-arm64-gnu@15.3.4':
|
||||
resolution: {integrity: sha512-l8ZQOCCg7adwmsnFm8m5q9eIPAHdaB2F3cxhufYtVo84pymwKuWfpYTKcUiFcutJdp9xGHC+F1Uq3xnFU1B/7g==}
|
||||
'@next/swc-linux-arm64-gnu@15.3.5':
|
||||
resolution: {integrity: sha512-LVD6uMOZ7XePg3KWYdGuzuvVboxujGjbcuP2jsPAN3MnLdLoZUXKRc6ixxfs03RH7qBdEHCZjyLP/jBdCJVRJQ==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@next/swc-linux-arm64-musl@15.3.4':
|
||||
resolution: {integrity: sha512-wFyZ7X470YJQtpKot4xCY3gpdn8lE9nTlldG07/kJYexCUpX1piX+MBfZdvulo+t1yADFVEuzFfVHfklfEx8kw==}
|
||||
'@next/swc-linux-arm64-musl@15.3.5':
|
||||
resolution: {integrity: sha512-k8aVScYZ++BnS2P69ClK7v4nOu702jcF9AIHKu6llhHEtBSmM2zkPGl9yoqbSU/657IIIb0QHpdxEr0iW9z53A==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@next/swc-linux-x64-gnu@15.3.4':
|
||||
resolution: {integrity: sha512-gEbH9rv9o7I12qPyvZNVTyP/PWKqOp8clvnoYZQiX800KkqsaJZuOXkWgMa7ANCCh/oEN2ZQheh3yH8/kWPSEg==}
|
||||
'@next/swc-linux-x64-gnu@15.3.5':
|
||||
resolution: {integrity: sha512-2xYU0DI9DGN/bAHzVwADid22ba5d/xrbrQlr2U+/Q5WkFUzeL0TDR963BdrtLS/4bMmKZGptLeg6282H/S2i8A==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@next/swc-linux-x64-musl@15.3.4':
|
||||
resolution: {integrity: sha512-Cf8sr0ufuC/nu/yQ76AnarbSAXcwG/wj+1xFPNbyNo8ltA6kw5d5YqO8kQuwVIxk13SBdtgXrNyom3ZosHAy4A==}
|
||||
'@next/swc-linux-x64-musl@15.3.5':
|
||||
resolution: {integrity: sha512-TRYIqAGf1KCbuAB0gjhdn5Ytd8fV+wJSM2Nh2is/xEqR8PZHxfQuaiNhoF50XfY90sNpaRMaGhF6E+qjV1b9Tg==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@next/swc-win32-arm64-msvc@15.3.4':
|
||||
resolution: {integrity: sha512-ay5+qADDN3rwRbRpEhTOreOn1OyJIXS60tg9WMYTWCy3fB6rGoyjLVxc4dR9PYjEdR2iDYsaF5h03NA+XuYPQQ==}
|
||||
'@next/swc-win32-arm64-msvc@15.3.5':
|
||||
resolution: {integrity: sha512-h04/7iMEUSMY6fDGCvdanKqlO1qYvzNxntZlCzfE8i5P0uqzVQWQquU1TIhlz0VqGQGXLrFDuTJVONpqGqjGKQ==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@next/swc-win32-x64-msvc@15.3.4':
|
||||
resolution: {integrity: sha512-4kDt31Bc9DGyYs41FTL1/kNpDeHyha2TC0j5sRRoKCyrhNcfZ/nRQkAUlF27mETwm8QyHqIjHJitfcza2Iykfg==}
|
||||
'@next/swc-win32-x64-msvc@15.3.5':
|
||||
resolution: {integrity: sha512-5fhH6fccXxnX2KhllnGhkYMndhOiLOLEiVGYjP2nizqeGWkN10sA9taATlXwake2E2XMvYZjjz0Uj7T0y+z1yw==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
@ -1669,8 +1669,8 @@ packages:
|
||||
resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
eslint-config-next@15.3.4:
|
||||
resolution: {integrity: sha512-WqeumCq57QcTP2lYlV6BRUySfGiBYEXlQ1L0mQ+u4N4X4ZhUVSSQ52WtjqHv60pJ6dD7jn+YZc0d1/ZSsxccvg==}
|
||||
eslint-config-next@15.3.6:
|
||||
resolution: {integrity: sha512-UylZINx8zjSgKHFn60h6Pjwgb40xkJ1ip9jfJ5t7D9/TJNnBIMoH5MtDWdEMatby3jiUB3twvk5cZgtOGOh9Qg==}
|
||||
peerDependencies:
|
||||
eslint: ^7.23.0 || ^8.0.0 || ^9.0.0
|
||||
typescript: '>=3.3.1'
|
||||
@ -2593,8 +2593,8 @@ packages:
|
||||
react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc
|
||||
react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc
|
||||
|
||||
next@15.3.4:
|
||||
resolution: {integrity: sha512-mHKd50C+mCjam/gcnwqL1T1vPx/XQNFlXqFIVdgQdVAFY9iIQtY0IfaVflEYzKiqjeA7B0cYYMaCrmAYFjs4rA==}
|
||||
next@15.3.6:
|
||||
resolution: {integrity: sha512-oI6D1zbbsh6JzzZFDCSHnnx6Qpvd1fSkVJu/5d8uluqnxzuoqtodVZjYvNovooznUq8udSAiKp7MbwlfZ8Gm6w==}
|
||||
engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
@ -3579,34 +3579,34 @@ snapshots:
|
||||
'@tybys/wasm-util': 0.9.0
|
||||
optional: true
|
||||
|
||||
'@next/env@15.3.4': {}
|
||||
'@next/env@15.3.6': {}
|
||||
|
||||
'@next/eslint-plugin-next@15.3.4':
|
||||
'@next/eslint-plugin-next@15.3.6':
|
||||
dependencies:
|
||||
fast-glob: 3.3.1
|
||||
|
||||
'@next/swc-darwin-arm64@15.3.4':
|
||||
'@next/swc-darwin-arm64@15.3.5':
|
||||
optional: true
|
||||
|
||||
'@next/swc-darwin-x64@15.3.4':
|
||||
'@next/swc-darwin-x64@15.3.5':
|
||||
optional: true
|
||||
|
||||
'@next/swc-linux-arm64-gnu@15.3.4':
|
||||
'@next/swc-linux-arm64-gnu@15.3.5':
|
||||
optional: true
|
||||
|
||||
'@next/swc-linux-arm64-musl@15.3.4':
|
||||
'@next/swc-linux-arm64-musl@15.3.5':
|
||||
optional: true
|
||||
|
||||
'@next/swc-linux-x64-gnu@15.3.4':
|
||||
'@next/swc-linux-x64-gnu@15.3.5':
|
||||
optional: true
|
||||
|
||||
'@next/swc-linux-x64-musl@15.3.4':
|
||||
'@next/swc-linux-x64-musl@15.3.5':
|
||||
optional: true
|
||||
|
||||
'@next/swc-win32-arm64-msvc@15.3.4':
|
||||
'@next/swc-win32-arm64-msvc@15.3.5':
|
||||
optional: true
|
||||
|
||||
'@next/swc-win32-x64-msvc@15.3.4':
|
||||
'@next/swc-win32-x64-msvc@15.3.5':
|
||||
optional: true
|
||||
|
||||
'@nodelib/fs.scandir@2.1.5':
|
||||
@ -4771,9 +4771,9 @@ snapshots:
|
||||
|
||||
escape-string-regexp@5.0.0: {}
|
||||
|
||||
eslint-config-next@15.3.4(eslint@9.30.0(jiti@2.4.2))(typescript@5.8.3):
|
||||
eslint-config-next@15.3.6(eslint@9.30.0(jiti@2.4.2))(typescript@5.8.3):
|
||||
dependencies:
|
||||
'@next/eslint-plugin-next': 15.3.4
|
||||
'@next/eslint-plugin-next': 15.3.6
|
||||
'@rushstack/eslint-patch': 1.12.0
|
||||
'@typescript-eslint/eslint-plugin': 8.35.1(@typescript-eslint/parser@8.35.1(eslint@9.30.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.30.0(jiti@2.4.2))(typescript@5.8.3)
|
||||
'@typescript-eslint/parser': 8.35.1(eslint@9.30.0(jiti@2.4.2))(typescript@5.8.3)
|
||||
@ -5085,7 +5085,7 @@ snapshots:
|
||||
react: 19.1.0
|
||||
react-dom: 19.1.0(react@19.1.0)
|
||||
|
||||
fumadocs-core@15.2.7(@types/react@19.1.8)(next@15.3.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
|
||||
fumadocs-core@15.2.7(@types/react@19.1.8)(next@15.3.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
|
||||
dependencies:
|
||||
'@formatjs/intl-localematcher': 0.6.1
|
||||
'@orama/orama': 3.1.10
|
||||
@ -5103,21 +5103,21 @@ snapshots:
|
||||
shiki: 3.7.0
|
||||
unist-util-visit: 5.0.0
|
||||
optionalDependencies:
|
||||
next: 15.3.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
next: 15.3.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
react: 19.1.0
|
||||
react-dom: 19.1.0(react@19.1.0)
|
||||
transitivePeerDependencies:
|
||||
- '@types/react'
|
||||
- supports-color
|
||||
|
||||
fumadocs-mdx@11.6.10(acorn@8.15.0)(fumadocs-core@15.2.7(@types/react@19.1.8)(next@15.3.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(next@15.3.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0)):
|
||||
fumadocs-mdx@11.6.10(acorn@8.15.0)(fumadocs-core@15.2.7(@types/react@19.1.8)(next@15.3.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(next@15.3.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0)):
|
||||
dependencies:
|
||||
'@mdx-js/mdx': 3.1.0(acorn@8.15.0)
|
||||
'@standard-schema/spec': 1.0.0
|
||||
chokidar: 4.0.3
|
||||
esbuild: 0.25.5
|
||||
estree-util-value-to-estree: 3.4.0
|
||||
fumadocs-core: 15.2.7(@types/react@19.1.8)(next@15.3.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
fumadocs-core: 15.2.7(@types/react@19.1.8)(next@15.3.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
js-yaml: 4.1.0
|
||||
lru-cache: 11.1.0
|
||||
picocolors: 1.1.1
|
||||
@ -5126,12 +5126,12 @@ snapshots:
|
||||
unist-util-visit: 5.0.0
|
||||
zod: 3.25.74
|
||||
optionalDependencies:
|
||||
next: 15.3.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
next: 15.3.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
transitivePeerDependencies:
|
||||
- acorn
|
||||
- supports-color
|
||||
|
||||
fumadocs-ui@15.2.7(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(next@15.3.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(tailwindcss@4.1.11):
|
||||
fumadocs-ui@15.2.7(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(next@15.3.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(tailwindcss@4.1.11):
|
||||
dependencies:
|
||||
'@radix-ui/react-accordion': 1.2.11(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
'@radix-ui/react-collapsible': 1.1.11(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
@ -5143,10 +5143,10 @@ snapshots:
|
||||
'@radix-ui/react-slot': 1.2.3(@types/react@19.1.8)(react@19.1.0)
|
||||
'@radix-ui/react-tabs': 1.1.12(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
class-variance-authority: 0.7.1
|
||||
fumadocs-core: 15.2.7(@types/react@19.1.8)(next@15.3.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
fumadocs-core: 15.2.7(@types/react@19.1.8)(next@15.3.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
lodash.merge: 4.6.2
|
||||
lucide-react: 0.487.0(react@19.1.0)
|
||||
next: 15.3.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
next: 15.3.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
next-themes: 0.4.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
postcss-selector-parser: 7.1.0
|
||||
react: 19.1.0
|
||||
@ -6076,9 +6076,9 @@ snapshots:
|
||||
react: 19.1.0
|
||||
react-dom: 19.1.0(react@19.1.0)
|
||||
|
||||
next@15.3.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
|
||||
next@15.3.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
|
||||
dependencies:
|
||||
'@next/env': 15.3.4
|
||||
'@next/env': 15.3.6
|
||||
'@swc/counter': 0.1.3
|
||||
'@swc/helpers': 0.5.15
|
||||
busboy: 1.6.0
|
||||
@ -6088,14 +6088,14 @@ snapshots:
|
||||
react-dom: 19.1.0(react@19.1.0)
|
||||
styled-jsx: 5.1.6(react@19.1.0)
|
||||
optionalDependencies:
|
||||
'@next/swc-darwin-arm64': 15.3.4
|
||||
'@next/swc-darwin-x64': 15.3.4
|
||||
'@next/swc-linux-arm64-gnu': 15.3.4
|
||||
'@next/swc-linux-arm64-musl': 15.3.4
|
||||
'@next/swc-linux-x64-gnu': 15.3.4
|
||||
'@next/swc-linux-x64-musl': 15.3.4
|
||||
'@next/swc-win32-arm64-msvc': 15.3.4
|
||||
'@next/swc-win32-x64-msvc': 15.3.4
|
||||
'@next/swc-darwin-arm64': 15.3.5
|
||||
'@next/swc-darwin-x64': 15.3.5
|
||||
'@next/swc-linux-arm64-gnu': 15.3.5
|
||||
'@next/swc-linux-arm64-musl': 15.3.5
|
||||
'@next/swc-linux-x64-gnu': 15.3.5
|
||||
'@next/swc-linux-x64-musl': 15.3.5
|
||||
'@next/swc-win32-arm64-msvc': 15.3.5
|
||||
'@next/swc-win32-x64-msvc': 15.3.5
|
||||
sharp: 0.34.2
|
||||
transitivePeerDependencies:
|
||||
- '@babel/core'
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "palmr-api",
|
||||
"version": "3.3.1-beta",
|
||||
"version": "3.3.2-beta",
|
||||
"description": "API for Palmr",
|
||||
"private": true,
|
||||
"author": "Daniel Luiz Alves <daniel@kyantech.com.br>",
|
||||
|
||||
@ -88,6 +88,9 @@ export const s3Client = hasValidConfig
|
||||
secretAccessKey: storageConfig.secretKey,
|
||||
},
|
||||
forcePathStyle: storageConfig.forcePathStyle,
|
||||
requestHandler: {
|
||||
requestTimeout: 300000, // 5 minutes timeout for S3 operations
|
||||
},
|
||||
})
|
||||
: null;
|
||||
|
||||
@ -139,5 +142,8 @@ export function createPublicS3Client(): S3Client | null {
|
||||
secretAccessKey: storageConfig.secretKey,
|
||||
},
|
||||
forcePathStyle: storageConfig.forcePathStyle,
|
||||
requestHandler: {
|
||||
requestTimeout: 300000, // 5 minutes timeout for S3 operations
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@ -313,6 +313,7 @@ export class FileController {
|
||||
|
||||
reply.header("Content-Type", contentType);
|
||||
reply.header("Content-Disposition", `inline; filename="${encodeURIComponent(fileName)}"`);
|
||||
reply.header("Content-Length", reverseShareFile.size.toString());
|
||||
|
||||
return reply.send(stream);
|
||||
}
|
||||
@ -369,6 +370,7 @@ export class FileController {
|
||||
|
||||
reply.header("Content-Type", contentType);
|
||||
reply.header("Content-Disposition", `inline; filename="${encodeURIComponent(fileName)}"`);
|
||||
reply.header("Content-Length", fileRecord.size.toString());
|
||||
|
||||
return reply.send(stream);
|
||||
} catch (error) {
|
||||
@ -612,6 +614,7 @@ export class FileController {
|
||||
|
||||
reply.header("Content-Type", contentType);
|
||||
reply.header("Content-Disposition", `inline; filename="${encodeURIComponent(fileName)}"`);
|
||||
reply.header("Content-Length", fileRecord.size.toString());
|
||||
reply.header("Cache-Control", "public, max-age=31536000"); // Cache por 1 ano
|
||||
|
||||
return reply.send(stream);
|
||||
|
||||
@ -486,6 +486,159 @@ export class ReverseShareController {
|
||||
}
|
||||
}
|
||||
|
||||
// Multipart upload endpoints for reverse shares
|
||||
async createMultipartUploadByAlias(request: FastifyRequest, reply: FastifyReply) {
|
||||
try {
|
||||
const { alias } = request.params as { alias: string };
|
||||
const { password } = request.query as { password?: string };
|
||||
const { filename, extension } = request.body as { filename: string; extension: string };
|
||||
|
||||
if (!filename || !extension) {
|
||||
return reply.status(400).send({ error: "filename and extension are required" });
|
||||
}
|
||||
|
||||
const result = await this.reverseShareService.createMultipartUploadByAlias(alias, filename, extension, password);
|
||||
return reply.status(200).send({
|
||||
uploadId: result.uploadId,
|
||||
objectName: result.objectName,
|
||||
message: "Multipart upload initialized",
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error("[Multipart] Create multipart upload error:", error);
|
||||
if (error.message === "Reverse share not found") {
|
||||
return reply.status(404).send({ error: error.message });
|
||||
}
|
||||
if (error.message === "Reverse share is inactive") {
|
||||
return reply.status(403).send({ error: error.message });
|
||||
}
|
||||
if (error.message === "Reverse share has expired") {
|
||||
return reply.status(410).send({ error: error.message });
|
||||
}
|
||||
if (error.message === "Password required" || error.message === "Invalid password") {
|
||||
return reply.status(401).send({ error: error.message });
|
||||
}
|
||||
return reply.status(500).send({ error: "Failed to create multipart upload" });
|
||||
}
|
||||
}
|
||||
|
||||
async getMultipartPartUrlByAlias(request: FastifyRequest, reply: FastifyReply) {
|
||||
try {
|
||||
const { alias } = request.params as { alias: string };
|
||||
const { password, uploadId, objectName, partNumber } = request.query as {
|
||||
password?: string;
|
||||
uploadId: string;
|
||||
objectName: string;
|
||||
partNumber: string;
|
||||
};
|
||||
|
||||
if (!uploadId || !objectName || !partNumber) {
|
||||
return reply.status(400).send({ error: "uploadId, objectName, and partNumber are required" });
|
||||
}
|
||||
|
||||
const partNum = parseInt(partNumber);
|
||||
if (isNaN(partNum) || partNum < 1 || partNum > 10000) {
|
||||
return reply.status(400).send({ error: "partNumber must be between 1 and 10000" });
|
||||
}
|
||||
|
||||
const result = await this.reverseShareService.getMultipartPartUrlByAlias(
|
||||
alias,
|
||||
uploadId,
|
||||
objectName,
|
||||
partNum,
|
||||
password
|
||||
);
|
||||
return reply.status(200).send({ url: result.url });
|
||||
} catch (error: any) {
|
||||
console.error("[Multipart] Get part URL error:", error);
|
||||
if (error.message === "Reverse share not found") {
|
||||
return reply.status(404).send({ error: error.message });
|
||||
}
|
||||
if (error.message === "Reverse share is inactive") {
|
||||
return reply.status(403).send({ error: error.message });
|
||||
}
|
||||
if (error.message === "Reverse share has expired") {
|
||||
return reply.status(410).send({ error: error.message });
|
||||
}
|
||||
if (error.message === "Password required" || error.message === "Invalid password") {
|
||||
return reply.status(401).send({ error: error.message });
|
||||
}
|
||||
return reply.status(500).send({ error: "Failed to get presigned URL for part" });
|
||||
}
|
||||
}
|
||||
|
||||
async completeMultipartUploadByAlias(request: FastifyRequest, reply: FastifyReply) {
|
||||
try {
|
||||
const { alias } = request.params as { alias: string };
|
||||
const { password } = request.query as { password?: string };
|
||||
const { uploadId, objectName, parts } = request.body as {
|
||||
uploadId: string;
|
||||
objectName: string;
|
||||
parts: Array<{ PartNumber: number; ETag: string }>;
|
||||
};
|
||||
|
||||
if (!uploadId || !objectName || !parts || !Array.isArray(parts)) {
|
||||
return reply.status(400).send({ error: "uploadId, objectName, and parts are required" });
|
||||
}
|
||||
|
||||
const result = await this.reverseShareService.completeMultipartUploadByAlias(
|
||||
alias,
|
||||
uploadId,
|
||||
objectName,
|
||||
parts,
|
||||
password
|
||||
);
|
||||
return reply.status(200).send(result);
|
||||
} catch (error: any) {
|
||||
console.error("[Multipart] Complete multipart upload error:", error);
|
||||
if (error.message === "Reverse share not found") {
|
||||
return reply.status(404).send({ error: error.message });
|
||||
}
|
||||
if (error.message === "Reverse share is inactive") {
|
||||
return reply.status(403).send({ error: error.message });
|
||||
}
|
||||
if (error.message === "Reverse share has expired") {
|
||||
return reply.status(410).send({ error: error.message });
|
||||
}
|
||||
if (error.message === "Password required" || error.message === "Invalid password") {
|
||||
return reply.status(401).send({ error: error.message });
|
||||
}
|
||||
return reply.status(500).send({ error: "Failed to complete multipart upload" });
|
||||
}
|
||||
}
|
||||
|
||||
async abortMultipartUploadByAlias(request: FastifyRequest, reply: FastifyReply) {
|
||||
try {
|
||||
const { alias } = request.params as { alias: string };
|
||||
const { password } = request.query as { password?: string };
|
||||
const { uploadId, objectName } = request.body as {
|
||||
uploadId: string;
|
||||
objectName: string;
|
||||
};
|
||||
|
||||
if (!uploadId || !objectName) {
|
||||
return reply.status(400).send({ error: "uploadId and objectName are required" });
|
||||
}
|
||||
|
||||
const result = await this.reverseShareService.abortMultipartUploadByAlias(alias, uploadId, objectName, password);
|
||||
return reply.status(200).send(result);
|
||||
} catch (error: any) {
|
||||
console.error("[Multipart] Abort multipart upload error:", error);
|
||||
if (error.message === "Reverse share not found") {
|
||||
return reply.status(404).send({ error: error.message });
|
||||
}
|
||||
if (error.message === "Reverse share is inactive") {
|
||||
return reply.status(403).send({ error: error.message });
|
||||
}
|
||||
if (error.message === "Reverse share has expired") {
|
||||
return reply.status(410).send({ error: error.message });
|
||||
}
|
||||
if (error.message === "Password required" || error.message === "Invalid password") {
|
||||
return reply.status(401).send({ error: error.message });
|
||||
}
|
||||
return reply.status(500).send({ error: "Failed to abort multipart upload" });
|
||||
}
|
||||
}
|
||||
|
||||
async getReverseShareMetadataByAlias(request: FastifyRequest, reply: FastifyReply) {
|
||||
try {
|
||||
const { alias } = request.params as { alias: string };
|
||||
|
||||
@ -593,6 +593,154 @@ export async function reverseShareRoutes(app: FastifyInstance) {
|
||||
reverseShareController.copyFileToUserFiles.bind(reverseShareController)
|
||||
);
|
||||
|
||||
// Multipart upload routes for reverse shares (public - no auth required)
|
||||
app.post(
|
||||
"/reverse-shares/alias/:alias/multipart/create",
|
||||
{
|
||||
schema: {
|
||||
tags: ["Reverse Share"],
|
||||
operationId: "createMultipartUploadByAlias",
|
||||
summary: "Create Multipart Upload for Reverse Share (Public)",
|
||||
description:
|
||||
"Initializes a multipart upload for large files (≥100MB) to a reverse share. Returns uploadId for subsequent part uploads.",
|
||||
params: z.object({
|
||||
alias: z.string().describe("Alias of the reverse share"),
|
||||
}),
|
||||
querystring: z.object({
|
||||
password: z.string().optional().describe("Password for accessing password-protected reverse shares"),
|
||||
}),
|
||||
body: z.object({
|
||||
filename: z.string().min(1).describe("The filename without extension"),
|
||||
extension: z.string().min(1).describe("The file extension"),
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
uploadId: z.string().describe("The upload ID for this multipart upload"),
|
||||
objectName: z.string().describe("The object name in storage"),
|
||||
message: z.string().describe("Success message"),
|
||||
}),
|
||||
400: z.object({ error: z.string() }),
|
||||
401: z.object({ error: z.string() }),
|
||||
403: z.object({ error: z.string() }),
|
||||
404: z.object({ error: z.string() }),
|
||||
410: z.object({ error: z.string() }),
|
||||
500: z.object({ error: z.string() }),
|
||||
},
|
||||
},
|
||||
},
|
||||
reverseShareController.createMultipartUploadByAlias.bind(reverseShareController)
|
||||
);
|
||||
|
||||
app.get(
|
||||
"/reverse-shares/alias/:alias/multipart/part-url",
|
||||
{
|
||||
schema: {
|
||||
tags: ["Reverse Share"],
|
||||
operationId: "getMultipartPartUrlByAlias",
|
||||
summary: "Get Presigned URL for Part (Public)",
|
||||
description: "Gets a presigned URL for uploading a specific part of a multipart upload to a reverse share",
|
||||
params: z.object({
|
||||
alias: z.string().describe("Alias of the reverse share"),
|
||||
}),
|
||||
querystring: z.object({
|
||||
password: z.string().optional().describe("Password for accessing password-protected reverse shares"),
|
||||
uploadId: z.string().min(1).describe("The multipart upload ID"),
|
||||
objectName: z.string().min(1).describe("The object name"),
|
||||
partNumber: z.string().min(1).describe("The part number (1-10000)"),
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
url: z.string().describe("The presigned URL for uploading this part"),
|
||||
}),
|
||||
400: z.object({ error: z.string() }),
|
||||
401: z.object({ error: z.string() }),
|
||||
403: z.object({ error: z.string() }),
|
||||
404: z.object({ error: z.string() }),
|
||||
410: z.object({ error: z.string() }),
|
||||
500: z.object({ error: z.string() }),
|
||||
},
|
||||
},
|
||||
},
|
||||
reverseShareController.getMultipartPartUrlByAlias.bind(reverseShareController)
|
||||
);
|
||||
|
||||
app.post(
|
||||
"/reverse-shares/alias/:alias/multipart/complete",
|
||||
{
|
||||
schema: {
|
||||
tags: ["Reverse Share"],
|
||||
operationId: "completeMultipartUploadByAlias",
|
||||
summary: "Complete Multipart Upload (Public)",
|
||||
description: "Completes a multipart upload to a reverse share by combining all uploaded parts",
|
||||
params: z.object({
|
||||
alias: z.string().describe("Alias of the reverse share"),
|
||||
}),
|
||||
querystring: z.object({
|
||||
password: z.string().optional().describe("Password for accessing password-protected reverse shares"),
|
||||
}),
|
||||
body: z.object({
|
||||
uploadId: z.string().min(1).describe("The multipart upload ID"),
|
||||
objectName: z.string().min(1).describe("The object name"),
|
||||
parts: z
|
||||
.array(
|
||||
z.object({
|
||||
PartNumber: z.number().min(1).max(10000).describe("The part number"),
|
||||
ETag: z.string().min(1).describe("The ETag returned from uploading the part"),
|
||||
})
|
||||
)
|
||||
.describe("Array of uploaded parts"),
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
message: z.string().describe("Success message"),
|
||||
objectName: z.string().describe("The completed object name"),
|
||||
}),
|
||||
400: z.object({ error: z.string() }),
|
||||
401: z.object({ error: z.string() }),
|
||||
403: z.object({ error: z.string() }),
|
||||
404: z.object({ error: z.string() }),
|
||||
410: z.object({ error: z.string() }),
|
||||
500: z.object({ error: z.string() }),
|
||||
},
|
||||
},
|
||||
},
|
||||
reverseShareController.completeMultipartUploadByAlias.bind(reverseShareController)
|
||||
);
|
||||
|
||||
app.post(
|
||||
"/reverse-shares/alias/:alias/multipart/abort",
|
||||
{
|
||||
schema: {
|
||||
tags: ["Reverse Share"],
|
||||
operationId: "abortMultipartUploadByAlias",
|
||||
summary: "Abort Multipart Upload (Public)",
|
||||
description: "Aborts a multipart upload to a reverse share and cleans up all uploaded parts",
|
||||
params: z.object({
|
||||
alias: z.string().describe("Alias of the reverse share"),
|
||||
}),
|
||||
querystring: z.object({
|
||||
password: z.string().optional().describe("Password for accessing password-protected reverse shares"),
|
||||
}),
|
||||
body: z.object({
|
||||
uploadId: z.string().min(1).describe("The multipart upload ID"),
|
||||
objectName: z.string().min(1).describe("The object name"),
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
message: z.string().describe("Success message"),
|
||||
}),
|
||||
400: z.object({ error: z.string() }),
|
||||
401: z.object({ error: z.string() }),
|
||||
403: z.object({ error: z.string() }),
|
||||
404: z.object({ error: z.string() }),
|
||||
410: z.object({ error: z.string() }),
|
||||
500: z.object({ error: z.string() }),
|
||||
},
|
||||
},
|
||||
},
|
||||
reverseShareController.abortMultipartUploadByAlias.bind(reverseShareController)
|
||||
);
|
||||
|
||||
app.get(
|
||||
"/reverse-shares/alias/:alias/metadata",
|
||||
{
|
||||
|
||||
@ -780,6 +780,101 @@ export class ReverseShareService {
|
||||
return result;
|
||||
}
|
||||
|
||||
// Helper method to validate reverse share access (reduces duplication)
|
||||
private async validateReverseShareAccessByAlias(alias: string, password?: string) {
|
||||
const reverseShare = await this.reverseShareRepository.findByAlias(alias);
|
||||
if (!reverseShare) {
|
||||
throw new Error("Reverse share not found");
|
||||
}
|
||||
|
||||
if (!reverseShare.isActive) {
|
||||
throw new Error("Reverse share is inactive");
|
||||
}
|
||||
|
||||
if (reverseShare.expiration && new Date(reverseShare.expiration) < new Date()) {
|
||||
throw new Error("Reverse share has expired");
|
||||
}
|
||||
|
||||
if (reverseShare.password) {
|
||||
if (!password) {
|
||||
throw new Error("Password required");
|
||||
}
|
||||
const isValidPassword = await this.reverseShareRepository.comparePassword(password, reverseShare.password);
|
||||
if (!isValidPassword) {
|
||||
throw new Error("Invalid password");
|
||||
}
|
||||
}
|
||||
|
||||
return reverseShare;
|
||||
}
|
||||
|
||||
// Multipart upload methods for reverse shares
|
||||
async createMultipartUploadByAlias(
|
||||
alias: string,
|
||||
filename: string,
|
||||
extension: string,
|
||||
password?: string
|
||||
): Promise<{ uploadId: string; objectName: string }> {
|
||||
await this.validateReverseShareAccessByAlias(alias, password);
|
||||
|
||||
// Generate unique object name using timestamp and random suffix
|
||||
const objectName = `reverse-shares/${alias}/${Date.now()}-${Math.random().toString(36).substring(7)}-${filename}.${extension}`;
|
||||
|
||||
const uploadId = await this.fileService.createMultipartUpload(objectName);
|
||||
|
||||
return {
|
||||
uploadId,
|
||||
objectName,
|
||||
};
|
||||
}
|
||||
|
||||
async getMultipartPartUrlByAlias(
|
||||
alias: string,
|
||||
uploadId: string,
|
||||
objectName: string,
|
||||
partNumber: number,
|
||||
password?: string
|
||||
): Promise<{ url: string }> {
|
||||
await this.validateReverseShareAccessByAlias(alias, password);
|
||||
|
||||
const expires = parseInt(env.PRESIGNED_URL_EXPIRATION);
|
||||
const url = await this.fileService.getPresignedPartUrl(objectName, uploadId, partNumber, expires);
|
||||
|
||||
return { url };
|
||||
}
|
||||
|
||||
async completeMultipartUploadByAlias(
|
||||
alias: string,
|
||||
uploadId: string,
|
||||
objectName: string,
|
||||
parts: Array<{ PartNumber: number; ETag: string }>,
|
||||
password?: string
|
||||
): Promise<{ message: string; objectName: string }> {
|
||||
await this.validateReverseShareAccessByAlias(alias, password);
|
||||
|
||||
await this.fileService.completeMultipartUpload(objectName, uploadId, parts);
|
||||
|
||||
return {
|
||||
message: "Multipart upload completed successfully",
|
||||
objectName,
|
||||
};
|
||||
}
|
||||
|
||||
async abortMultipartUploadByAlias(
|
||||
alias: string,
|
||||
uploadId: string,
|
||||
objectName: string,
|
||||
password?: string
|
||||
): Promise<{ message: string }> {
|
||||
await this.validateReverseShareAccessByAlias(alias, password);
|
||||
|
||||
await this.fileService.abortMultipartUpload(objectName, uploadId);
|
||||
|
||||
return {
|
||||
message: "Multipart upload aborted successfully",
|
||||
};
|
||||
}
|
||||
|
||||
private formatFileResponse(file: any) {
|
||||
return {
|
||||
id: file.id,
|
||||
|
||||
@ -90,7 +90,10 @@ export class S3StorageProvider implements StorageProvider {
|
||||
Key: objectName,
|
||||
});
|
||||
|
||||
return await getSignedUrl(client, command, { expiresIn: expires });
|
||||
return await getSignedUrl(client, command, {
|
||||
expiresIn: expires,
|
||||
unsignableHeaders: new Set(["x-amz-checksum-crc32"]),
|
||||
});
|
||||
}
|
||||
|
||||
async getPresignedGetUrl(objectName: string, expires: number, fileName?: string): Promise<string> {
|
||||
@ -179,10 +182,7 @@ export class S3StorageProvider implements StorageProvider {
|
||||
* Returns uploadId for subsequent part uploads
|
||||
*/
|
||||
async createMultipartUpload(objectName: string): Promise<string> {
|
||||
const client = createPublicS3Client();
|
||||
if (!client) {
|
||||
throw new Error("S3 client could not be created");
|
||||
}
|
||||
const client = this.ensureClient();
|
||||
|
||||
const command = new CreateMultipartUploadCommand({
|
||||
Bucket: bucketName,
|
||||
@ -219,7 +219,10 @@ export class S3StorageProvider implements StorageProvider {
|
||||
PartNumber: partNumber,
|
||||
});
|
||||
|
||||
const url = await getSignedUrl(client, command, { expiresIn: expires });
|
||||
const url = await getSignedUrl(client, command, {
|
||||
expiresIn: expires,
|
||||
unsignableHeaders: new Set(["x-amz-checksum-crc32"]),
|
||||
});
|
||||
return url;
|
||||
}
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "palmr-web",
|
||||
"version": "3.3.1-beta",
|
||||
"version": "3.3.2-beta",
|
||||
"description": "Frontend for Palmr",
|
||||
"private": true,
|
||||
"author": "Daniel Luiz Alves <daniel@kyantech.com.br>",
|
||||
@ -63,7 +63,7 @@
|
||||
"jszip": "^3.10.1",
|
||||
"lucide-react": "^0.525.0",
|
||||
"nanoid": "^5.1.5",
|
||||
"next": "15.3.4",
|
||||
"next": "15.3.6",
|
||||
"next-intl": "^4.3.1",
|
||||
"next-themes": "^0.4.6",
|
||||
"nookies": "^2.5.2",
|
||||
@ -96,7 +96,7 @@
|
||||
"@typescript-eslint/eslint-plugin": "8.35.1",
|
||||
"@typescript-eslint/parser": "8.35.1",
|
||||
"eslint": "9.30.0",
|
||||
"eslint-config-next": "15.3.4",
|
||||
"eslint-config-next": "15.3.6",
|
||||
"eslint-config-prettier": "9.1.0",
|
||||
"eslint-plugin-prettier": "5.5.1",
|
||||
"prettier": "3.6.2",
|
||||
|
||||
106
apps/web/pnpm-lock.yaml
generated
106
apps/web/pnpm-lock.yaml
generated
@ -105,11 +105,11 @@ importers:
|
||||
specifier: ^5.1.5
|
||||
version: 5.1.5
|
||||
next:
|
||||
specifier: 15.3.4
|
||||
version: 15.3.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
specifier: 15.3.6
|
||||
version: 15.3.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
next-intl:
|
||||
specifier: ^4.3.1
|
||||
version: 4.3.4(next@15.3.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0)(typescript@5.8.3)
|
||||
version: 4.3.4(next@15.3.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0)(typescript@5.8.3)
|
||||
next-themes:
|
||||
specifier: ^0.4.6
|
||||
version: 0.4.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
@ -199,8 +199,8 @@ importers:
|
||||
specifier: 9.30.0
|
||||
version: 9.30.0(jiti@2.4.2)
|
||||
eslint-config-next:
|
||||
specifier: 15.3.4
|
||||
version: 15.3.4(eslint@9.30.0(jiti@2.4.2))(typescript@5.8.3)
|
||||
specifier: 15.3.6
|
||||
version: 15.3.6(eslint@9.30.0(jiti@2.4.2))(typescript@5.8.3)
|
||||
eslint-config-prettier:
|
||||
specifier: 9.1.0
|
||||
version: 9.1.0(eslint@9.30.0(jiti@2.4.2))
|
||||
@ -531,56 +531,56 @@ packages:
|
||||
'@napi-rs/wasm-runtime@0.2.11':
|
||||
resolution: {integrity: sha512-9DPkXtvHydrcOsopiYpUgPHpmj0HWZKMUnL2dZqpvC42lsratuBG06V5ipyno0fUek5VlFsNQ+AcFATSrJXgMA==}
|
||||
|
||||
'@next/env@15.3.4':
|
||||
resolution: {integrity: sha512-ZkdYzBseS6UjYzz6ylVKPOK+//zLWvD6Ta+vpoye8cW11AjiQjGYVibF0xuvT4L0iJfAPfZLFidaEzAOywyOAQ==}
|
||||
'@next/env@15.3.6':
|
||||
resolution: {integrity: sha512-/cK+QPcfRbDZxmI/uckT4lu9pHCfRIPBLqy88MhE+7Vg5hKrEYc333Ae76dn/cw2FBP2bR/GoK/4DU+U7by/Nw==}
|
||||
|
||||
'@next/eslint-plugin-next@15.3.4':
|
||||
resolution: {integrity: sha512-lBxYdj7TI8phbJcLSAqDt57nIcobEign5NYIKCiy0hXQhrUbTqLqOaSDi568U6vFg4hJfBdZYsG4iP/uKhCqgg==}
|
||||
'@next/eslint-plugin-next@15.3.6':
|
||||
resolution: {integrity: sha512-gvt7l1r4N0zHCXyXYj39ObrTBr8TxyA/306Z/kjseYk6hiefu3zexRKRVjVmQqUpxe9oxyfYWMZFtsBYPgr1oA==}
|
||||
|
||||
'@next/swc-darwin-arm64@15.3.4':
|
||||
resolution: {integrity: sha512-z0qIYTONmPRbwHWvpyrFXJd5F9YWLCsw3Sjrzj2ZvMYy9NPQMPZ1NjOJh4ojr4oQzcGYwgJKfidzehaNa1BpEg==}
|
||||
'@next/swc-darwin-arm64@15.3.5':
|
||||
resolution: {integrity: sha512-lM/8tilIsqBq+2nq9kbTW19vfwFve0NR7MxfkuSUbRSgXlMQoJYg+31+++XwKVSXk4uT23G2eF/7BRIKdn8t8w==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@next/swc-darwin-x64@15.3.4':
|
||||
resolution: {integrity: sha512-Z0FYJM8lritw5Wq+vpHYuCIzIlEMjewG2aRkc3Hi2rcbULknYL/xqfpBL23jQnCSrDUGAo/AEv0Z+s2bff9Zkw==}
|
||||
'@next/swc-darwin-x64@15.3.5':
|
||||
resolution: {integrity: sha512-WhwegPQJ5IfoUNZUVsI9TRAlKpjGVK0tpJTL6KeiC4cux9774NYE9Wu/iCfIkL/5J8rPAkqZpG7n+EfiAfidXA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@next/swc-linux-arm64-gnu@15.3.4':
|
||||
resolution: {integrity: sha512-l8ZQOCCg7adwmsnFm8m5q9eIPAHdaB2F3cxhufYtVo84pymwKuWfpYTKcUiFcutJdp9xGHC+F1Uq3xnFU1B/7g==}
|
||||
'@next/swc-linux-arm64-gnu@15.3.5':
|
||||
resolution: {integrity: sha512-LVD6uMOZ7XePg3KWYdGuzuvVboxujGjbcuP2jsPAN3MnLdLoZUXKRc6ixxfs03RH7qBdEHCZjyLP/jBdCJVRJQ==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@next/swc-linux-arm64-musl@15.3.4':
|
||||
resolution: {integrity: sha512-wFyZ7X470YJQtpKot4xCY3gpdn8lE9nTlldG07/kJYexCUpX1piX+MBfZdvulo+t1yADFVEuzFfVHfklfEx8kw==}
|
||||
'@next/swc-linux-arm64-musl@15.3.5':
|
||||
resolution: {integrity: sha512-k8aVScYZ++BnS2P69ClK7v4nOu702jcF9AIHKu6llhHEtBSmM2zkPGl9yoqbSU/657IIIb0QHpdxEr0iW9z53A==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@next/swc-linux-x64-gnu@15.3.4':
|
||||
resolution: {integrity: sha512-gEbH9rv9o7I12qPyvZNVTyP/PWKqOp8clvnoYZQiX800KkqsaJZuOXkWgMa7ANCCh/oEN2ZQheh3yH8/kWPSEg==}
|
||||
'@next/swc-linux-x64-gnu@15.3.5':
|
||||
resolution: {integrity: sha512-2xYU0DI9DGN/bAHzVwADid22ba5d/xrbrQlr2U+/Q5WkFUzeL0TDR963BdrtLS/4bMmKZGptLeg6282H/S2i8A==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@next/swc-linux-x64-musl@15.3.4':
|
||||
resolution: {integrity: sha512-Cf8sr0ufuC/nu/yQ76AnarbSAXcwG/wj+1xFPNbyNo8ltA6kw5d5YqO8kQuwVIxk13SBdtgXrNyom3ZosHAy4A==}
|
||||
'@next/swc-linux-x64-musl@15.3.5':
|
||||
resolution: {integrity: sha512-TRYIqAGf1KCbuAB0gjhdn5Ytd8fV+wJSM2Nh2is/xEqR8PZHxfQuaiNhoF50XfY90sNpaRMaGhF6E+qjV1b9Tg==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@next/swc-win32-arm64-msvc@15.3.4':
|
||||
resolution: {integrity: sha512-ay5+qADDN3rwRbRpEhTOreOn1OyJIXS60tg9WMYTWCy3fB6rGoyjLVxc4dR9PYjEdR2iDYsaF5h03NA+XuYPQQ==}
|
||||
'@next/swc-win32-arm64-msvc@15.3.5':
|
||||
resolution: {integrity: sha512-h04/7iMEUSMY6fDGCvdanKqlO1qYvzNxntZlCzfE8i5P0uqzVQWQquU1TIhlz0VqGQGXLrFDuTJVONpqGqjGKQ==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@next/swc-win32-x64-msvc@15.3.4':
|
||||
resolution: {integrity: sha512-4kDt31Bc9DGyYs41FTL1/kNpDeHyha2TC0j5sRRoKCyrhNcfZ/nRQkAUlF27mETwm8QyHqIjHJitfcza2Iykfg==}
|
||||
'@next/swc-win32-x64-msvc@15.3.5':
|
||||
resolution: {integrity: sha512-5fhH6fccXxnX2KhllnGhkYMndhOiLOLEiVGYjP2nizqeGWkN10sA9taATlXwake2E2XMvYZjjz0Uj7T0y+z1yw==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
@ -1818,8 +1818,8 @@ packages:
|
||||
resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
eslint-config-next@15.3.4:
|
||||
resolution: {integrity: sha512-WqeumCq57QcTP2lYlV6BRUySfGiBYEXlQ1L0mQ+u4N4X4ZhUVSSQ52WtjqHv60pJ6dD7jn+YZc0d1/ZSsxccvg==}
|
||||
eslint-config-next@15.3.6:
|
||||
resolution: {integrity: sha512-UylZINx8zjSgKHFn60h6Pjwgb40xkJ1ip9jfJ5t7D9/TJNnBIMoH5MtDWdEMatby3jiUB3twvk5cZgtOGOh9Qg==}
|
||||
peerDependencies:
|
||||
eslint: ^7.23.0 || ^8.0.0 || ^9.0.0
|
||||
typescript: '>=3.3.1'
|
||||
@ -2539,8 +2539,8 @@ packages:
|
||||
react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc
|
||||
react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc
|
||||
|
||||
next@15.3.4:
|
||||
resolution: {integrity: sha512-mHKd50C+mCjam/gcnwqL1T1vPx/XQNFlXqFIVdgQdVAFY9iIQtY0IfaVflEYzKiqjeA7B0cYYMaCrmAYFjs4rA==}
|
||||
next@15.3.6:
|
||||
resolution: {integrity: sha512-oI6D1zbbsh6JzzZFDCSHnnx6Qpvd1fSkVJu/5d8uluqnxzuoqtodVZjYvNovooznUq8udSAiKp7MbwlfZ8Gm6w==}
|
||||
engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
@ -3540,34 +3540,34 @@ snapshots:
|
||||
'@tybys/wasm-util': 0.9.0
|
||||
optional: true
|
||||
|
||||
'@next/env@15.3.4': {}
|
||||
'@next/env@15.3.6': {}
|
||||
|
||||
'@next/eslint-plugin-next@15.3.4':
|
||||
'@next/eslint-plugin-next@15.3.6':
|
||||
dependencies:
|
||||
fast-glob: 3.3.1
|
||||
|
||||
'@next/swc-darwin-arm64@15.3.4':
|
||||
'@next/swc-darwin-arm64@15.3.5':
|
||||
optional: true
|
||||
|
||||
'@next/swc-darwin-x64@15.3.4':
|
||||
'@next/swc-darwin-x64@15.3.5':
|
||||
optional: true
|
||||
|
||||
'@next/swc-linux-arm64-gnu@15.3.4':
|
||||
'@next/swc-linux-arm64-gnu@15.3.5':
|
||||
optional: true
|
||||
|
||||
'@next/swc-linux-arm64-musl@15.3.4':
|
||||
'@next/swc-linux-arm64-musl@15.3.5':
|
||||
optional: true
|
||||
|
||||
'@next/swc-linux-x64-gnu@15.3.4':
|
||||
'@next/swc-linux-x64-gnu@15.3.5':
|
||||
optional: true
|
||||
|
||||
'@next/swc-linux-x64-musl@15.3.4':
|
||||
'@next/swc-linux-x64-musl@15.3.5':
|
||||
optional: true
|
||||
|
||||
'@next/swc-win32-arm64-msvc@15.3.4':
|
||||
'@next/swc-win32-arm64-msvc@15.3.5':
|
||||
optional: true
|
||||
|
||||
'@next/swc-win32-x64-msvc@15.3.4':
|
||||
'@next/swc-win32-x64-msvc@15.3.5':
|
||||
optional: true
|
||||
|
||||
'@nodelib/fs.scandir@2.1.5':
|
||||
@ -4886,9 +4886,9 @@ snapshots:
|
||||
|
||||
escape-string-regexp@4.0.0: {}
|
||||
|
||||
eslint-config-next@15.3.4(eslint@9.30.0(jiti@2.4.2))(typescript@5.8.3):
|
||||
eslint-config-next@15.3.6(eslint@9.30.0(jiti@2.4.2))(typescript@5.8.3):
|
||||
dependencies:
|
||||
'@next/eslint-plugin-next': 15.3.4
|
||||
'@next/eslint-plugin-next': 15.3.6
|
||||
'@rushstack/eslint-patch': 1.12.0
|
||||
'@typescript-eslint/eslint-plugin': 8.35.1(@typescript-eslint/parser@8.35.1(eslint@9.30.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.30.0(jiti@2.4.2))(typescript@5.8.3)
|
||||
'@typescript-eslint/parser': 8.35.1(eslint@9.30.0(jiti@2.4.2))(typescript@5.8.3)
|
||||
@ -5618,11 +5618,11 @@ snapshots:
|
||||
|
||||
negotiator@1.0.0: {}
|
||||
|
||||
next-intl@4.3.4(next@15.3.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0)(typescript@5.8.3):
|
||||
next-intl@4.3.4(next@15.3.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0)(typescript@5.8.3):
|
||||
dependencies:
|
||||
'@formatjs/intl-localematcher': 0.5.10
|
||||
negotiator: 1.0.0
|
||||
next: 15.3.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
next: 15.3.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
react: 19.1.0
|
||||
use-intl: 4.3.4(react@19.1.0)
|
||||
optionalDependencies:
|
||||
@ -5633,9 +5633,9 @@ snapshots:
|
||||
react: 19.1.0
|
||||
react-dom: 19.1.0(react@19.1.0)
|
||||
|
||||
next@15.3.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
|
||||
next@15.3.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
|
||||
dependencies:
|
||||
'@next/env': 15.3.4
|
||||
'@next/env': 15.3.6
|
||||
'@swc/counter': 0.1.3
|
||||
'@swc/helpers': 0.5.15
|
||||
busboy: 1.6.0
|
||||
@ -5645,14 +5645,14 @@ snapshots:
|
||||
react-dom: 19.1.0(react@19.1.0)
|
||||
styled-jsx: 5.1.6(react@19.1.0)
|
||||
optionalDependencies:
|
||||
'@next/swc-darwin-arm64': 15.3.4
|
||||
'@next/swc-darwin-x64': 15.3.4
|
||||
'@next/swc-linux-arm64-gnu': 15.3.4
|
||||
'@next/swc-linux-arm64-musl': 15.3.4
|
||||
'@next/swc-linux-x64-gnu': 15.3.4
|
||||
'@next/swc-linux-x64-musl': 15.3.4
|
||||
'@next/swc-win32-arm64-msvc': 15.3.4
|
||||
'@next/swc-win32-x64-msvc': 15.3.4
|
||||
'@next/swc-darwin-arm64': 15.3.5
|
||||
'@next/swc-darwin-x64': 15.3.5
|
||||
'@next/swc-linux-arm64-gnu': 15.3.5
|
||||
'@next/swc-linux-arm64-musl': 15.3.5
|
||||
'@next/swc-linux-x64-gnu': 15.3.5
|
||||
'@next/swc-linux-x64-musl': 15.3.5
|
||||
'@next/swc-win32-arm64-msvc': 15.3.5
|
||||
'@next/swc-win32-x64-msvc': 15.3.5
|
||||
sharp: 0.34.2
|
||||
transitivePeerDependencies:
|
||||
- '@babel/core'
|
||||
|
||||
@ -13,7 +13,14 @@ import { Label } from "@/components/ui/label";
|
||||
import { Progress } from "@/components/ui/progress";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { useUppyUpload } from "@/hooks/useUppyUpload";
|
||||
import { getPresignedUrlForUploadByAlias, registerFileUploadByAlias } from "@/http/endpoints";
|
||||
import {
|
||||
abortMultipartUploadByAlias,
|
||||
completeMultipartUploadByAlias,
|
||||
createMultipartUploadByAlias,
|
||||
getMultipartPartUrlByAlias,
|
||||
getPresignedUrlForUploadByAlias,
|
||||
registerFileUploadByAlias,
|
||||
} from "@/http/endpoints";
|
||||
import { formatFileSize } from "@/utils/format-file-size";
|
||||
import { UPLOAD_CONFIG } from "../constants";
|
||||
import { FileUploadSectionProps } from "../types";
|
||||
@ -102,6 +109,41 @@ export function FileUploadSection({ reverseShare, password, alias, onUploadSucce
|
||||
onUploadSuccess?.();
|
||||
}
|
||||
},
|
||||
// Custom multipart functions for reverse share uploads (no auth required)
|
||||
customMultipartFunctions: {
|
||||
createMultipartUpload: async (filename: string, extension: string) => {
|
||||
const response = await createMultipartUploadByAlias(
|
||||
alias,
|
||||
{ filename, extension },
|
||||
password ? { password } : undefined
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
getMultipartPartUrl: async (uploadId: string, objectName: string, partNumber: string) => {
|
||||
const response = await getMultipartPartUrlByAlias(alias, { uploadId, objectName, partNumber, password });
|
||||
return response.data;
|
||||
},
|
||||
completeMultipartUpload: async (
|
||||
uploadId: string,
|
||||
objectName: string,
|
||||
parts: Array<{ PartNumber: number; ETag: string }>
|
||||
) => {
|
||||
const response = await completeMultipartUploadByAlias(
|
||||
alias,
|
||||
{ uploadId, objectName, parts },
|
||||
password ? { password } : undefined
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
abortMultipartUpload: async (uploadId: string, objectName: string) => {
|
||||
const response = await abortMultipartUploadByAlias(
|
||||
alias,
|
||||
{ uploadId, objectName },
|
||||
password ? { password } : undefined
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const onDrop = useCallback(
|
||||
|
||||
@ -45,9 +45,10 @@ async function getBaseUrl(): Promise<string> {
|
||||
return `${protocol}://${host}`;
|
||||
}
|
||||
|
||||
export async function generateMetadata({ params }: { params: { alias: string } }): Promise<Metadata> {
|
||||
export async function generateMetadata({ params }: { params: Promise<{ alias: string }> }): Promise<Metadata> {
|
||||
const t = await getTranslations();
|
||||
const metadata = await getReverseShareMetadata(params.alias);
|
||||
const resolvedParams = await params;
|
||||
const metadata = await getReverseShareMetadata(resolvedParams.alias);
|
||||
const appInfo = await getAppInfo();
|
||||
|
||||
const title = metadata?.name || t("reverseShares.upload.metadata.title");
|
||||
@ -58,7 +59,7 @@ export async function generateMetadata({ params }: { params: { alias: string } }
|
||||
: t("reverseShares.upload.metadata.description"));
|
||||
|
||||
const baseUrl = await getBaseUrl();
|
||||
const shareUrl = `${baseUrl}/r/${params.alias}`;
|
||||
const shareUrl = `${baseUrl}/r/${resolvedParams.alias}`;
|
||||
|
||||
return {
|
||||
title,
|
||||
|
||||
@ -335,64 +335,71 @@ export function usePublicShare() {
|
||||
}
|
||||
|
||||
try {
|
||||
// Prepare all items for the share-specific bulk download
|
||||
const allItems: Array<{
|
||||
objectName?: string;
|
||||
name: string;
|
||||
id?: string;
|
||||
type?: "file" | "folder";
|
||||
}> = [];
|
||||
|
||||
if (share.files) {
|
||||
share.files.forEach((file) => {
|
||||
if (!file.folderId) {
|
||||
allItems.push({
|
||||
objectName: file.objectName,
|
||||
name: file.name,
|
||||
type: "file",
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (share.folders) {
|
||||
const folderIds = new Set(share.folders.map((f) => f.id));
|
||||
share.folders.forEach((folder) => {
|
||||
if (!folder.parentId || !folderIds.has(folder.parentId)) {
|
||||
allItems.push({
|
||||
id: folder.id,
|
||||
name: folder.name,
|
||||
type: "folder",
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (allItems.length === 0) {
|
||||
toast.error(t("shareManager.noFilesToDownload"));
|
||||
return;
|
||||
}
|
||||
|
||||
const loadingToast = toast.loading(t("shareManager.creatingZip"));
|
||||
|
||||
try {
|
||||
// Get presigned URLs for all files
|
||||
const downloadItems = await Promise.all(
|
||||
allItems
|
||||
.filter((item) => item.type === "file" && item.objectName)
|
||||
.map(async (item) => {
|
||||
// Helper function to get all files in a folder recursively with paths
|
||||
const getFolderFilesWithPath = (
|
||||
targetFolderId: string,
|
||||
currentPath: string = ""
|
||||
): Array<{ file: any; path: string }> => {
|
||||
const filesWithPath: Array<{ file: any; path: string }> = [];
|
||||
|
||||
// Get direct files in this folder
|
||||
const directFiles = share.files?.filter((f) => f.folderId === targetFolderId) || [];
|
||||
directFiles.forEach((file) => {
|
||||
filesWithPath.push({ file, path: currentPath });
|
||||
});
|
||||
|
||||
// Get subfolders and process them recursively
|
||||
const subfolders = share.folders?.filter((f) => f.parentId === targetFolderId) || [];
|
||||
for (const subfolder of subfolders) {
|
||||
const subfolderPath = currentPath ? `${currentPath}/${subfolder.name}` : subfolder.name;
|
||||
filesWithPath.push(...getFolderFilesWithPath(subfolder.id, subfolderPath));
|
||||
}
|
||||
|
||||
return filesWithPath;
|
||||
};
|
||||
|
||||
const allFilesToDownload: Array<{ url: string; name: string }> = [];
|
||||
|
||||
// Get presigned URLs for root level files (not in any folder)
|
||||
const rootFiles = share.files?.filter((f) => !f.folderId) || [];
|
||||
const rootFileItems = await Promise.all(
|
||||
rootFiles.map(async (file) => {
|
||||
const url = await getCachedDownloadUrl(
|
||||
file.objectName,
|
||||
password ? { headers: { "x-share-password": password } } : undefined
|
||||
);
|
||||
return {
|
||||
url,
|
||||
name: file.name,
|
||||
};
|
||||
})
|
||||
);
|
||||
allFilesToDownload.push(...rootFileItems);
|
||||
|
||||
// Get presigned URLs for files in root level folders
|
||||
const rootFolders = share.folders?.filter((f) => !f.parentId) || [];
|
||||
for (const folder of rootFolders) {
|
||||
const folderFilesWithPath = getFolderFilesWithPath(folder.id, folder.name);
|
||||
|
||||
const folderFileItems = await Promise.all(
|
||||
folderFilesWithPath.map(async ({ file, path }) => {
|
||||
const url = await getCachedDownloadUrl(
|
||||
item.objectName!,
|
||||
file.objectName,
|
||||
password ? { headers: { "x-share-password": password } } : undefined
|
||||
);
|
||||
return {
|
||||
url,
|
||||
name: item.name,
|
||||
name: path ? `${path}/${file.name}` : file.name,
|
||||
};
|
||||
})
|
||||
);
|
||||
);
|
||||
allFilesToDownload.push(...folderFileItems);
|
||||
}
|
||||
|
||||
if (downloadItems.length === 0) {
|
||||
if (allFilesToDownload.length === 0) {
|
||||
toast.dismiss(loadingToast);
|
||||
toast.error(t("shareManager.noFilesToDownload"));
|
||||
return;
|
||||
@ -401,7 +408,7 @@ export function usePublicShare() {
|
||||
// Create ZIP with all files
|
||||
const { downloadFilesAsZip } = await import("@/utils/zip-download");
|
||||
const zipName = `${share.name || t("shareManager.defaultShareName")}.zip`;
|
||||
await downloadFilesAsZip(downloadItems, zipName);
|
||||
await downloadFilesAsZip(allFilesToDownload, zipName);
|
||||
|
||||
toast.dismiss(loadingToast);
|
||||
toast.success(t("shareManager.zipDownloadSuccess"));
|
||||
|
||||
@ -4,7 +4,7 @@ import { getTranslations } from "next-intl/server";
|
||||
|
||||
interface LayoutProps {
|
||||
children: React.ReactNode;
|
||||
params: { alias: string };
|
||||
params: Promise<{ alias: string }>;
|
||||
}
|
||||
|
||||
async function getShareMetadata(alias: string) {
|
||||
@ -50,9 +50,10 @@ async function getBaseUrl(): Promise<string> {
|
||||
return `${protocol}://${host}`;
|
||||
}
|
||||
|
||||
export async function generateMetadata({ params }: { params: { alias: string } }): Promise<Metadata> {
|
||||
export async function generateMetadata({ params }: { params: Promise<{ alias: string }> }): Promise<Metadata> {
|
||||
const t = await getTranslations();
|
||||
const metadata = await getShareMetadata(params.alias);
|
||||
const resolvedParams = await params;
|
||||
const metadata = await getShareMetadata(resolvedParams.alias);
|
||||
const appInfo = await getAppInfo();
|
||||
|
||||
const title = metadata?.name || t("share.pageTitle");
|
||||
@ -63,7 +64,7 @@ export async function generateMetadata({ params }: { params: { alias: string } }
|
||||
: appInfo.appDescription || t("share.metadata.defaultDescription"));
|
||||
|
||||
const baseUrl = await getBaseUrl();
|
||||
const shareUrl = `${baseUrl}/s/${params.alias}`;
|
||||
const shareUrl = `${baseUrl}/s/${resolvedParams.alias}`;
|
||||
|
||||
return {
|
||||
title,
|
||||
|
||||
@ -121,7 +121,7 @@ export function TwoFactorForm() {
|
||||
<CardDescription>{status.enabled ? t("twoFactor.enabled") : t("twoFactor.description")}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between sm:gap-0">
|
||||
<div>
|
||||
<p className="font-medium">
|
||||
{t("twoFactor.status.label")}{" "}
|
||||
@ -133,19 +133,29 @@ export function TwoFactorForm() {
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<div className="flex flex-col sm:flex-row gap-2 w-full sm:w-auto">
|
||||
{status.enabled ? (
|
||||
<>
|
||||
<Button variant="outline" onClick={generateNewBackupCodes} disabled={isLoading}>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={generateNewBackupCodes}
|
||||
disabled={isLoading}
|
||||
className="w-full sm:w-auto"
|
||||
>
|
||||
<IconKey className="h-4 w-4" />
|
||||
{t("twoFactor.backupCodes.generateNew")}
|
||||
</Button>
|
||||
<Button variant="destructive" onClick={() => setIsDisableModalOpen(true)} disabled={isLoading}>
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={() => setIsDisableModalOpen(true)}
|
||||
disabled={isLoading}
|
||||
className="w-full sm:w-auto"
|
||||
>
|
||||
{t("twoFactor.buttons.disable2FA")}
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<Button onClick={startSetup} disabled={isLoading}>
|
||||
<Button onClick={startSetup} disabled={isLoading} className="w-full sm:w-auto">
|
||||
<IconShield className="h-4 w-4" />
|
||||
{t("twoFactor.buttons.enable2FA")}
|
||||
</Button>
|
||||
|
||||
@ -1,56 +1,33 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import * as React from "react"
|
||||
import * as ContextMenuPrimitive from "@radix-ui/react-context-menu"
|
||||
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
|
||||
import * as React from "react";
|
||||
import * as ContextMenuPrimitive from "@radix-ui/react-context-menu";
|
||||
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function ContextMenu({
|
||||
...props
|
||||
}: React.ComponentProps<typeof ContextMenuPrimitive.Root>) {
|
||||
return <ContextMenuPrimitive.Root data-slot="context-menu" {...props} />
|
||||
function ContextMenu({ ...props }: React.ComponentProps<typeof ContextMenuPrimitive.Root>) {
|
||||
return <ContextMenuPrimitive.Root data-slot="context-menu" {...props} />;
|
||||
}
|
||||
|
||||
function ContextMenuTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof ContextMenuPrimitive.Trigger>) {
|
||||
return (
|
||||
<ContextMenuPrimitive.Trigger data-slot="context-menu-trigger" {...props} />
|
||||
)
|
||||
function ContextMenuTrigger({ ...props }: React.ComponentProps<typeof ContextMenuPrimitive.Trigger>) {
|
||||
return <ContextMenuPrimitive.Trigger data-slot="context-menu-trigger" {...props} />;
|
||||
}
|
||||
|
||||
function ContextMenuGroup({
|
||||
...props
|
||||
}: React.ComponentProps<typeof ContextMenuPrimitive.Group>) {
|
||||
return (
|
||||
<ContextMenuPrimitive.Group data-slot="context-menu-group" {...props} />
|
||||
)
|
||||
function ContextMenuGroup({ ...props }: React.ComponentProps<typeof ContextMenuPrimitive.Group>) {
|
||||
return <ContextMenuPrimitive.Group data-slot="context-menu-group" {...props} />;
|
||||
}
|
||||
|
||||
function ContextMenuPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof ContextMenuPrimitive.Portal>) {
|
||||
return (
|
||||
<ContextMenuPrimitive.Portal data-slot="context-menu-portal" {...props} />
|
||||
)
|
||||
function ContextMenuPortal({ ...props }: React.ComponentProps<typeof ContextMenuPrimitive.Portal>) {
|
||||
return <ContextMenuPrimitive.Portal data-slot="context-menu-portal" {...props} />;
|
||||
}
|
||||
|
||||
function ContextMenuSub({
|
||||
...props
|
||||
}: React.ComponentProps<typeof ContextMenuPrimitive.Sub>) {
|
||||
return <ContextMenuPrimitive.Sub data-slot="context-menu-sub" {...props} />
|
||||
function ContextMenuSub({ ...props }: React.ComponentProps<typeof ContextMenuPrimitive.Sub>) {
|
||||
return <ContextMenuPrimitive.Sub data-slot="context-menu-sub" {...props} />;
|
||||
}
|
||||
|
||||
function ContextMenuRadioGroup({
|
||||
...props
|
||||
}: React.ComponentProps<typeof ContextMenuPrimitive.RadioGroup>) {
|
||||
return (
|
||||
<ContextMenuPrimitive.RadioGroup
|
||||
data-slot="context-menu-radio-group"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
function ContextMenuRadioGroup({ ...props }: React.ComponentProps<typeof ContextMenuPrimitive.RadioGroup>) {
|
||||
return <ContextMenuPrimitive.RadioGroup data-slot="context-menu-radio-group" {...props} />;
|
||||
}
|
||||
|
||||
function ContextMenuSubTrigger({
|
||||
@ -59,7 +36,7 @@ function ContextMenuSubTrigger({
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ContextMenuPrimitive.SubTrigger> & {
|
||||
inset?: boolean
|
||||
inset?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<ContextMenuPrimitive.SubTrigger
|
||||
@ -74,13 +51,10 @@ function ContextMenuSubTrigger({
|
||||
{children}
|
||||
<ChevronRightIcon className="ml-auto" />
|
||||
</ContextMenuPrimitive.SubTrigger>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function ContextMenuSubContent({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ContextMenuPrimitive.SubContent>) {
|
||||
function ContextMenuSubContent({ className, ...props }: React.ComponentProps<typeof ContextMenuPrimitive.SubContent>) {
|
||||
return (
|
||||
<ContextMenuPrimitive.SubContent
|
||||
data-slot="context-menu-sub-content"
|
||||
@ -90,13 +64,10 @@ function ContextMenuSubContent({
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function ContextMenuContent({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ContextMenuPrimitive.Content>) {
|
||||
function ContextMenuContent({ className, ...props }: React.ComponentProps<typeof ContextMenuPrimitive.Content>) {
|
||||
return (
|
||||
<ContextMenuPrimitive.Portal>
|
||||
<ContextMenuPrimitive.Content
|
||||
@ -108,7 +79,7 @@ function ContextMenuContent({
|
||||
{...props}
|
||||
/>
|
||||
</ContextMenuPrimitive.Portal>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function ContextMenuItem({
|
||||
@ -117,8 +88,8 @@ function ContextMenuItem({
|
||||
variant = "default",
|
||||
...props
|
||||
}: React.ComponentProps<typeof ContextMenuPrimitive.Item> & {
|
||||
inset?: boolean
|
||||
variant?: "default" | "destructive"
|
||||
inset?: boolean;
|
||||
variant?: "default" | "destructive";
|
||||
}) {
|
||||
return (
|
||||
<ContextMenuPrimitive.Item
|
||||
@ -131,7 +102,7 @@ function ContextMenuItem({
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function ContextMenuCheckboxItem({
|
||||
@ -157,7 +128,7 @@ function ContextMenuCheckboxItem({
|
||||
</span>
|
||||
{children}
|
||||
</ContextMenuPrimitive.CheckboxItem>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function ContextMenuRadioItem({
|
||||
@ -181,7 +152,7 @@ function ContextMenuRadioItem({
|
||||
</span>
|
||||
{children}
|
||||
</ContextMenuPrimitive.RadioItem>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function ContextMenuLabel({
|
||||
@ -189,48 +160,36 @@ function ContextMenuLabel({
|
||||
inset,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ContextMenuPrimitive.Label> & {
|
||||
inset?: boolean
|
||||
inset?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<ContextMenuPrimitive.Label
|
||||
data-slot="context-menu-label"
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
"text-foreground px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
|
||||
className
|
||||
)}
|
||||
className={cn("text-foreground px-2 py-1.5 text-sm font-medium data-[inset]:pl-8", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function ContextMenuSeparator({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ContextMenuPrimitive.Separator>) {
|
||||
function ContextMenuSeparator({ className, ...props }: React.ComponentProps<typeof ContextMenuPrimitive.Separator>) {
|
||||
return (
|
||||
<ContextMenuPrimitive.Separator
|
||||
data-slot="context-menu-separator"
|
||||
className={cn("bg-border -mx-1 my-1 h-px", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function ContextMenuShortcut({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"span">) {
|
||||
function ContextMenuShortcut({ className, ...props }: React.ComponentProps<"span">) {
|
||||
return (
|
||||
<span
|
||||
data-slot="context-menu-shortcut"
|
||||
className={cn(
|
||||
"text-muted-foreground ml-auto text-xs tracking-widest",
|
||||
className
|
||||
)}
|
||||
className={cn("text-muted-foreground ml-auto text-xs tracking-widest", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
@ -249,4 +208,4 @@ export {
|
||||
ContextMenuSubContent,
|
||||
ContextMenuSubTrigger,
|
||||
ContextMenuRadioGroup,
|
||||
}
|
||||
};
|
||||
|
||||
@ -4,7 +4,6 @@ import Link from "next/link";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
import { useSecureConfigValue } from "@/hooks/use-secure-configs";
|
||||
|
||||
import packageJson from "../../../package.json";
|
||||
|
||||
const { version } = packageJson;
|
||||
|
||||
@ -5,6 +5,7 @@ const apiInstance = axios.create({
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
withCredentials: true,
|
||||
timeout: 120000, // 2 minutes timeout for API calls
|
||||
});
|
||||
|
||||
export default apiInstance;
|
||||
|
||||
@ -394,6 +394,7 @@ export function useEnhancedFileManager(
|
||||
await registerFolder(folderData);
|
||||
toast.success(t("folderActions.folderCreated"));
|
||||
setCreateFolderModalOpen(false);
|
||||
await onRefresh();
|
||||
} catch (error) {
|
||||
console.error("Error creating folder:", error);
|
||||
toast.error(t("folderActions.createFolderError"));
|
||||
@ -406,6 +407,7 @@ export function useEnhancedFileManager(
|
||||
await updateFolder(folderId, { name: newName, description });
|
||||
toast.success(t("folderActions.folderRenamed"));
|
||||
setFolderToRename(null);
|
||||
await onRefresh();
|
||||
} catch (error) {
|
||||
console.error("Error renaming folder:", error);
|
||||
toast.error(t("folderActions.renameFolderError"));
|
||||
@ -414,7 +416,6 @@ export function useEnhancedFileManager(
|
||||
|
||||
const handleFolderDelete = async (folderId: string) => {
|
||||
try {
|
||||
// Optimistic update - remove from UI immediately
|
||||
if (handleImmediateUpdate) {
|
||||
handleImmediateUpdate(folderId, "folder", "__DELETE__" as any);
|
||||
}
|
||||
|
||||
@ -4,6 +4,7 @@ import { toast } from "sonner";
|
||||
|
||||
import { getCachedDownloadUrl, getCachedReverseShareDownloadUrl } from "@/lib/download-url-cache";
|
||||
import { getFileExtension, getFileType, type FileType } from "@/utils/file-types";
|
||||
import { getMimeType } from "@/utils/mime-types";
|
||||
|
||||
interface FilePreviewState {
|
||||
previewUrl: string | null;
|
||||
@ -73,35 +74,49 @@ export function useFilePreview({ file, isOpen, isReverseShare = false, sharePass
|
||||
});
|
||||
}, []);
|
||||
|
||||
const loadVideoPreview = useCallback(async (url: string) => {
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
const loadVideoPreview = useCallback(
|
||||
async (url: string) => {
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const blob = await response.blob();
|
||||
// Create a new blob with the correct MIME type based on file extension
|
||||
// This fixes issues when the server returns application/octet-stream
|
||||
const mimeType = getMimeType(file.name);
|
||||
const typedBlob = new Blob([blob], { type: mimeType });
|
||||
const blobUrl = URL.createObjectURL(typedBlob);
|
||||
setState((prev) => ({ ...prev, videoBlob: blobUrl }));
|
||||
} catch {
|
||||
setState((prev) => ({ ...prev, previewUrl: url }));
|
||||
}
|
||||
},
|
||||
[file.name]
|
||||
);
|
||||
|
||||
const blob = await response.blob();
|
||||
const blobUrl = URL.createObjectURL(blob);
|
||||
setState((prev) => ({ ...prev, videoBlob: blobUrl }));
|
||||
} catch {
|
||||
setState((prev) => ({ ...prev, previewUrl: url }));
|
||||
}
|
||||
}, []);
|
||||
const loadAudioPreview = useCallback(
|
||||
async (url: string) => {
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const loadAudioPreview = useCallback(async (url: string) => {
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
const blob = await response.blob();
|
||||
// Create a new blob with the correct MIME type based on file extension
|
||||
// This fixes issues when the server returns application/octet-stream
|
||||
const mimeType = getMimeType(file.name);
|
||||
const typedBlob = new Blob([blob], { type: mimeType });
|
||||
const blobUrl = URL.createObjectURL(typedBlob);
|
||||
setState((prev) => ({ ...prev, previewUrl: blobUrl }));
|
||||
} catch {
|
||||
setState((prev) => ({ ...prev, previewUrl: url }));
|
||||
}
|
||||
|
||||
const blob = await response.blob();
|
||||
const blobUrl = URL.createObjectURL(blob);
|
||||
setState((prev) => ({ ...prev, previewUrl: blobUrl }));
|
||||
} catch {
|
||||
setState((prev) => ({ ...prev, previewUrl: url }));
|
||||
}
|
||||
}, []);
|
||||
},
|
||||
[file.name]
|
||||
);
|
||||
|
||||
const handlePdfLoadError = useCallback(() => {
|
||||
setState((prev) => {
|
||||
|
||||
@ -12,6 +12,20 @@ import {
|
||||
getMultipartPartUrl,
|
||||
} from "@/http/endpoints/files";
|
||||
|
||||
/**
|
||||
* Custom multipart upload functions for non-authenticated uploads (e.g., reverse shares)
|
||||
*/
|
||||
export interface CustomMultipartFunctions {
|
||||
createMultipartUpload: (filename: string, extension: string) => Promise<{ uploadId: string; objectName: string }>;
|
||||
getMultipartPartUrl: (uploadId: string, objectName: string, partNumber: string) => Promise<{ url: string }>;
|
||||
completeMultipartUpload: (
|
||||
uploadId: string,
|
||||
objectName: string,
|
||||
parts: Array<{ PartNumber: number; ETag: string }>
|
||||
) => Promise<any>;
|
||||
abortMultipartUpload: (uploadId: string, objectName: string) => Promise<any>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for the useUppyUpload hook
|
||||
*/
|
||||
@ -49,6 +63,11 @@ export interface UseUppyUploadOptions {
|
||||
* Optional folder context for file organization
|
||||
*/
|
||||
currentFolderId?: string;
|
||||
|
||||
/**
|
||||
* Optional custom multipart upload functions (for unauthenticated uploads like reverse shares)
|
||||
*/
|
||||
customMultipartFunctions?: CustomMultipartFunctions;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -85,6 +104,7 @@ export function useUppyUpload(options: UseUppyUploadOptions) {
|
||||
const onAfterUploadRef = useRef(options.onAfterUpload);
|
||||
const getPresignedUrlRef = useRef(options.getPresignedUrl);
|
||||
const onSuccessRef = useRef(options.onSuccess);
|
||||
const customMultipartRef = useRef(options.customMultipartFunctions);
|
||||
|
||||
// Update refs when callbacks change
|
||||
useEffect(() => {
|
||||
@ -93,6 +113,7 @@ export function useUppyUpload(options: UseUppyUploadOptions) {
|
||||
onAfterUploadRef.current = options.onAfterUpload;
|
||||
getPresignedUrlRef.current = options.getPresignedUrl;
|
||||
onSuccessRef.current = options.onSuccess;
|
||||
customMultipartRef.current = options.customMultipartFunctions;
|
||||
}, [options]);
|
||||
|
||||
// Initialize Uppy instance only once
|
||||
@ -109,8 +130,8 @@ export function useUppyUpload(options: UseUppyUploadOptions) {
|
||||
// Files <100MB: Use simple PUT upload
|
||||
// Files ≥100MB: Use multipart chunked upload
|
||||
uppy.use(AwsS3, {
|
||||
limit: 6, // Allow 6 concurrent part uploads
|
||||
getChunkSize: () => 8 * 1024 * 1024, // 8MB chunk size
|
||||
limit: 3, // Allow 3 concurrent part uploads (reduced for stability)
|
||||
getChunkSize: () => 5 * 1024 * 1024, // 5MB chunk size (reduced for better performance)
|
||||
shouldUseMultipart: (file: any) => {
|
||||
const fileSize = file.size || 0;
|
||||
const useMultipart = fileSize >= UPLOAD_CONFIG.MULTIPART_THRESHOLD;
|
||||
@ -184,12 +205,21 @@ export function useUppyUpload(options: UseUppyUploadOptions) {
|
||||
const filename = objectName.replace(`.${extension}`, "");
|
||||
|
||||
// 3. Create multipart upload on backend
|
||||
const response = await createMultipartUpload({
|
||||
filename,
|
||||
extension,
|
||||
});
|
||||
let response;
|
||||
if (customMultipartRef.current) {
|
||||
// Use custom multipart functions (e.g., for reverse shares)
|
||||
response = await customMultipartRef.current.createMultipartUpload(filename, extension);
|
||||
} else {
|
||||
// Use default authenticated multipart upload
|
||||
response = (
|
||||
await createMultipartUpload({
|
||||
filename,
|
||||
extension,
|
||||
})
|
||||
).data;
|
||||
}
|
||||
|
||||
const { uploadId, objectName: actualObjectName } = response.data;
|
||||
const { uploadId, objectName: actualObjectName } = response;
|
||||
|
||||
// Store metadata
|
||||
uppy.setFileMeta(file.id, {
|
||||
@ -221,15 +251,24 @@ export function useUppyUpload(options: UseUppyUploadOptions) {
|
||||
const { uploadId, key, partNumber } = partData;
|
||||
|
||||
try {
|
||||
const response = await getMultipartPartUrl({
|
||||
uploadId,
|
||||
objectName: key,
|
||||
partNumber: partNumber.toString(),
|
||||
});
|
||||
let response;
|
||||
if (customMultipartRef.current) {
|
||||
// Use custom multipart functions (e.g., for reverse shares)
|
||||
response = await customMultipartRef.current.getMultipartPartUrl(uploadId, key, partNumber.toString());
|
||||
} else {
|
||||
// Use default authenticated multipart upload
|
||||
response = (
|
||||
await getMultipartPartUrl({
|
||||
uploadId,
|
||||
objectName: key,
|
||||
partNumber: partNumber.toString(),
|
||||
})
|
||||
).data;
|
||||
}
|
||||
|
||||
// Return the signed URL object directly - Uppy expects { url, headers }
|
||||
return {
|
||||
url: response.data.url,
|
||||
url: response.url,
|
||||
headers: {},
|
||||
};
|
||||
} catch (error) {
|
||||
@ -244,11 +283,17 @@ export function useUppyUpload(options: UseUppyUploadOptions) {
|
||||
const meta = file.meta as { objectName: string };
|
||||
|
||||
try {
|
||||
await completeMultipartUpload({
|
||||
uploadId,
|
||||
objectName: meta.objectName || key,
|
||||
parts,
|
||||
});
|
||||
if (customMultipartRef.current) {
|
||||
// Use custom multipart functions (e.g., for reverse shares)
|
||||
await customMultipartRef.current.completeMultipartUpload(uploadId, meta.objectName || key, parts);
|
||||
} else {
|
||||
// Use default authenticated multipart upload
|
||||
await completeMultipartUpload({
|
||||
uploadId,
|
||||
objectName: meta.objectName || key,
|
||||
parts,
|
||||
});
|
||||
}
|
||||
|
||||
return {};
|
||||
} catch (error) {
|
||||
@ -262,10 +307,16 @@ export function useUppyUpload(options: UseUppyUploadOptions) {
|
||||
const meta = file.meta as { objectName: string };
|
||||
|
||||
try {
|
||||
await abortMultipartUpload({
|
||||
uploadId,
|
||||
objectName: meta.objectName || key,
|
||||
});
|
||||
if (customMultipartRef.current) {
|
||||
// Use custom multipart functions (e.g., for reverse shares)
|
||||
await customMultipartRef.current.abortMultipartUpload(uploadId, meta.objectName || key);
|
||||
} else {
|
||||
// Use default authenticated multipart upload
|
||||
await abortMultipartUpload({
|
||||
uploadId,
|
||||
objectName: meta.objectName || key,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[Upload:Multipart] Failed to abort multipart upload:", error);
|
||||
// Don't throw - abort is cleanup, shouldn't fail the operation
|
||||
|
||||
@ -273,3 +273,66 @@ export const copyReverseShareFileToUserFiles = <TData = any>(
|
||||
): Promise<TData> => {
|
||||
return apiInstance.post(`/api/reverse-shares/files/${fileId}/copy`, undefined, options);
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a multipart upload for reverse share (public endpoint)
|
||||
* @summary Create Multipart Upload for Reverse Share (Public)
|
||||
*/
|
||||
export const createMultipartUploadByAlias = <TData = any>(
|
||||
alias: string,
|
||||
body: { filename: string; extension: string },
|
||||
params?: { password?: string },
|
||||
options?: AxiosRequestConfig
|
||||
): Promise<TData> => {
|
||||
return apiInstance.post(`/api/reverse-shares/alias/${alias}/multipart/create`, body, {
|
||||
...options,
|
||||
params: { ...params, ...options?.params },
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Get presigned URL for a multipart upload part for reverse share (public endpoint)
|
||||
* @summary Get Multipart Part URL for Reverse Share (Public)
|
||||
*/
|
||||
export const getMultipartPartUrlByAlias = <TData = any>(
|
||||
alias: string,
|
||||
params: { uploadId: string; objectName: string; partNumber: string; password?: string },
|
||||
options?: AxiosRequestConfig
|
||||
): Promise<TData> => {
|
||||
return apiInstance.get(`/api/reverse-shares/alias/${alias}/multipart/part-url`, {
|
||||
...options,
|
||||
params: { ...params, ...options?.params },
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Complete a multipart upload for reverse share (public endpoint)
|
||||
* @summary Complete Multipart Upload for Reverse Share (Public)
|
||||
*/
|
||||
export const completeMultipartUploadByAlias = <TData = any>(
|
||||
alias: string,
|
||||
body: { uploadId: string; objectName: string; parts: Array<{ PartNumber: number; ETag: string }> },
|
||||
params?: { password?: string },
|
||||
options?: AxiosRequestConfig
|
||||
): Promise<TData> => {
|
||||
return apiInstance.post(`/api/reverse-shares/alias/${alias}/multipart/complete`, body, {
|
||||
...options,
|
||||
params: { ...params, ...options?.params },
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Abort a multipart upload for reverse share (public endpoint)
|
||||
* @summary Abort Multipart Upload for Reverse Share (Public)
|
||||
*/
|
||||
export const abortMultipartUploadByAlias = <TData = any>(
|
||||
alias: string,
|
||||
body: { uploadId: string; objectName: string },
|
||||
params?: { password?: string },
|
||||
options?: AxiosRequestConfig
|
||||
): Promise<TData> => {
|
||||
return apiInstance.post(`/api/reverse-shares/alias/${alias}/multipart/abort`, body, {
|
||||
...options,
|
||||
params: { ...params, ...options?.params },
|
||||
});
|
||||
};
|
||||
|
||||
@ -38,20 +38,42 @@ if [ $RETRY_COUNT -eq $MAX_RETRIES ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Configure storage client (mc) - ALWAYS reconfigure with current password
|
||||
# Configure storage client (mc) - Run as target UID/GID
|
||||
echo "[STORAGE-SYSTEM-SETUP] Configuring storage client..."
|
||||
mc alias set palmr-local http://127.0.0.1:9379 "$MINIO_ROOT_USER" "$MINIO_ROOT_PASSWORD" 2>/dev/null || {
|
||||
echo "[STORAGE-SYSTEM-SETUP] ✗ Failed to configure storage client"
|
||||
exit 1
|
||||
|
||||
# Get target UID/GID from environment or default
|
||||
TARGET_UID=${PALMR_UID:-${MINIO_UID:-1001}}
|
||||
TARGET_GID=${PALMR_GID:-${MINIO_GID:-1001}}
|
||||
|
||||
# Ensure home directory exists with correct ownership
|
||||
if [ "$(id -u)" = "0" ]; then
|
||||
mkdir -p /home/palmr/.mc
|
||||
chown -R $TARGET_UID:$TARGET_GID /home/palmr 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# Run mc commands as target user
|
||||
run_as_target() {
|
||||
if [ "$(id -u)" = "0" ]; then
|
||||
su-exec $TARGET_UID:$TARGET_GID "$@"
|
||||
else
|
||||
"$@"
|
||||
fi
|
||||
}
|
||||
|
||||
# Configure with verbose error output
|
||||
if ! run_as_target mc alias set palmr-local http://127.0.0.1:9379 "$MINIO_ROOT_USER" "$MINIO_ROOT_PASSWORD" 2>&1; then
|
||||
echo "[STORAGE-SYSTEM-SETUP] ✗ Failed to configure storage client"
|
||||
echo "[STORAGE-SYSTEM-SETUP] Debug: UID/GID=$TARGET_UID:$TARGET_GID, User=$(whoami)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Create bucket (idempotent - won't fail if exists)
|
||||
echo "[STORAGE-SYSTEM-SETUP] Ensuring storage bucket exists: $MINIO_BUCKET..."
|
||||
if mc ls palmr-local/$MINIO_BUCKET > /dev/null 2>&1; then
|
||||
if run_as_target mc ls palmr-local/$MINIO_BUCKET > /dev/null 2>&1; then
|
||||
echo "[STORAGE-SYSTEM-SETUP] ✓ Bucket '$MINIO_BUCKET' already exists"
|
||||
else
|
||||
echo "[STORAGE-SYSTEM-SETUP] Creating bucket '$MINIO_BUCKET'..."
|
||||
mc mb "palmr-local/$MINIO_BUCKET" 2>/dev/null || {
|
||||
run_as_target mc mb "palmr-local/$MINIO_BUCKET" 2>/dev/null || {
|
||||
echo "[STORAGE-SYSTEM-SETUP] ✗ Failed to create bucket"
|
||||
exit 1
|
||||
}
|
||||
@ -60,7 +82,7 @@ fi
|
||||
|
||||
# Set bucket policy to private (always reapply)
|
||||
echo "[STORAGE-SYSTEM-SETUP] Setting bucket policy..."
|
||||
mc anonymous set none "palmr-local/$MINIO_BUCKET" 2>/dev/null || true
|
||||
run_as_target mc anonymous set none "palmr-local/$MINIO_BUCKET" 2>/dev/null || true
|
||||
|
||||
# Save credentials for Palmr to use
|
||||
echo "[STORAGE-SYSTEM-SETUP] Saving credentials to $MINIO_CREDENTIALS..."
|
||||
|
||||
@ -38,12 +38,15 @@ if [ -n "$PALMR_UID" ] || [ -n "$PALMR_GID" ]; then
|
||||
echo "🔧 Runtime UID/GID: $TARGET_UID:$TARGET_GID"
|
||||
|
||||
echo "🔐 Updating file ownership..."
|
||||
chown -R $TARGET_UID:$TARGET_GID /app/palmr-app 2>/dev/null || echo "⚠️ Some ownership changes may have failed"
|
||||
|
||||
# Only chown application files (these are small and fast)
|
||||
find /app/palmr-app -maxdepth 2 -exec chown $TARGET_UID:$TARGET_GID {} + 2>/dev/null || echo "⚠️ Some app ownership changes may have failed"
|
||||
|
||||
# Home directory is small, safe to chown
|
||||
chown -R $TARGET_UID:$TARGET_GID /home/palmr 2>/dev/null || echo "⚠️ Some home directory ownership changes may have failed"
|
||||
|
||||
if [ -d "/app/server" ]; then
|
||||
chown -R $TARGET_UID:$TARGET_GID /app/server 2>/dev/null || echo "⚠️ Some data directory ownership changes may have failed"
|
||||
fi
|
||||
# /app/server is handled by the main startup script with smart marker
|
||||
# No need to duplicate the work here
|
||||
|
||||
echo "✅ UID/GID configuration completed"
|
||||
fi
|
||||
@ -59,10 +62,20 @@ echo "📁 Creating data directories..."
|
||||
mkdir -p /app/server/prisma /app/server/uploads /app/server/temp-uploads
|
||||
|
||||
if [ "$(id -u)" = "0" ]; then
|
||||
echo "🔐 Ensuring proper ownership for all operations..."
|
||||
# Fix permissions for entire /app/server to allow migration and storage system operations
|
||||
chown -R $TARGET_UID:$TARGET_GID /app/server 2>/dev/null || true
|
||||
chmod -R 755 /app/server 2>/dev/null || true
|
||||
echo "🔐 Ensuring proper ownership for critical files..."
|
||||
# Ensure base directories exist and have correct ownership
|
||||
chown $TARGET_UID:$TARGET_GID /app/server/uploads /app/server/temp-uploads 2>/dev/null || true
|
||||
chmod 755 /app/server/uploads /app/server/temp-uploads 2>/dev/null || true
|
||||
|
||||
# Critical: Database files need read+write permissions
|
||||
if [ -d "/app/server/prisma" ]; then
|
||||
chown -R $TARGET_UID:$TARGET_GID /app/server/prisma 2>/dev/null || true
|
||||
chmod -R 755 /app/server/prisma 2>/dev/null || true
|
||||
# Ensure database file is writable
|
||||
if [ -f "/app/server/prisma/palmr.db" ]; then
|
||||
chmod 644 /app/server/prisma/palmr.db 2>/dev/null || true
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
run_as_user() {
|
||||
|
||||
@ -10,41 +10,39 @@ MINIO_USER="palmr"
|
||||
|
||||
echo "[STORAGE-SYSTEM] Initializing storage..."
|
||||
|
||||
# DYNAMIC: Detect palmr user's actual UID and GID
|
||||
# This works with any Docker user configuration
|
||||
MINIO_UID=$(id -u $MINIO_USER 2>/dev/null || echo "1001")
|
||||
MINIO_GID=$(id -g $MINIO_USER 2>/dev/null || echo "1001")
|
||||
# USE ENVIRONMENT VARIABLES: Allow runtime UID/GID configuration
|
||||
# Falls back to palmr user's UID/GID if not specified
|
||||
export MINIO_UID=${PALMR_UID:-$(id -u $MINIO_USER 2>/dev/null || echo "1001")}
|
||||
export MINIO_GID=${PALMR_GID:-$(id -g $MINIO_USER 2>/dev/null || echo "1001")}
|
||||
|
||||
echo "[STORAGE-SYSTEM] Target user: $MINIO_USER (UID:$MINIO_UID, GID:$MINIO_GID)"
|
||||
|
||||
# CRITICAL: Fix permissions as root (supervisor runs this as root via user=root)
|
||||
# This MUST happen before dropping to palmr user
|
||||
# Ensure directory exists and has correct permissions
|
||||
if [ "$(id -u)" = "0" ]; then
|
||||
echo "[STORAGE-SYSTEM] Fixing permissions (running as root)..."
|
||||
mkdir -p "$DATA_DIR"
|
||||
|
||||
# Clean metadata
|
||||
# Clean metadata if exists
|
||||
if [ -d "$DATA_DIR/.minio.sys" ]; then
|
||||
echo "[STORAGE-SYSTEM] Cleaning metadata..."
|
||||
rm -rf "$DATA_DIR/.minio.sys" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# Ensure directory exists
|
||||
mkdir -p "$DATA_DIR"
|
||||
# CRITICAL: MinIO needs write access to ALL subdirectories for multipart uploads
|
||||
# Check if ownership is correct before doing expensive recursive chown
|
||||
CURRENT_OWNER=$(stat -c '%u:%g' "$DATA_DIR" 2>/dev/null || stat -f '%u:%g' "$DATA_DIR" 2>/dev/null || echo "0:0")
|
||||
TARGET_OWNER="${MINIO_UID}:${MINIO_GID}"
|
||||
|
||||
# FIX: Change ownership to palmr (using detected UID:GID)
|
||||
chown -R ${MINIO_UID}:${MINIO_GID} "$DATA_DIR" 2>/dev/null || {
|
||||
echo "[STORAGE-SYSTEM] ⚠️ chown -R failed, trying non-recursive..."
|
||||
chown ${MINIO_UID}:${MINIO_GID} "$DATA_DIR" 2>/dev/null || true
|
||||
}
|
||||
if [ "$CURRENT_OWNER" != "$TARGET_OWNER" ] || [ -n "$(find "$DATA_DIR" -type f -o -type d ! -user ${MINIO_UID} 2>/dev/null | head -1)" ]; then
|
||||
echo "[STORAGE-SYSTEM] Fixing storage permissions recursively..."
|
||||
chown -R ${MINIO_UID}:${MINIO_GID} "$DATA_DIR" 2>/dev/null || true
|
||||
echo "[STORAGE-SYSTEM] ✓ Permissions fixed (owner: ${MINIO_UID}:${MINIO_GID})"
|
||||
else
|
||||
echo "[STORAGE-SYSTEM] ✓ Permissions already correct"
|
||||
fi
|
||||
|
||||
chmod 755 "$DATA_DIR" 2>/dev/null || true
|
||||
|
||||
# Force filesystem sync to ensure changes are visible immediately
|
||||
sync
|
||||
|
||||
echo "[STORAGE-SYSTEM] ✓ Permissions fixed (owner: ${MINIO_UID}:${MINIO_GID})"
|
||||
else
|
||||
echo "[STORAGE-SYSTEM] ⚠️ WARNING: Not running as root, cannot fix permissions"
|
||||
echo "[STORAGE-SYSTEM] ⚠️ WARNING: Not running as root"
|
||||
fi
|
||||
|
||||
# Verify directory is writable (test as palmr with detected UID:GID)
|
||||
|
||||
@ -23,7 +23,7 @@ startsecs=3
|
||||
[program:minio-setup]
|
||||
command=/bin/sh /app/minio-setup.sh
|
||||
directory=/app
|
||||
user=palmr
|
||||
user=root
|
||||
autostart=true
|
||||
autorestart=unexpected
|
||||
exitcodes=0
|
||||
@ -31,7 +31,7 @@ stdout_logfile=/dev/stdout
|
||||
stdout_logfile_maxbytes=0
|
||||
stderr_logfile=/dev/stderr
|
||||
stderr_logfile_maxbytes=0
|
||||
environment=HOME="/home/palmr"
|
||||
environment=HOME="/root"
|
||||
priority=60
|
||||
startsecs=0
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "palmr-monorepo",
|
||||
"version": "3.3.1-beta",
|
||||
"version": "3.3.2-beta",
|
||||
"description": "Palmr monorepo with Husky configuration",
|
||||
"private": true,
|
||||
"packageManager": "pnpm@10.6.0",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user