diff --git a/CHANGELOG.md b/CHANGELOG.md index 3821f5b..55faa87 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,59 @@ # Changelog +## Changes 02/19/2026 (v3.5.1) + +`release(v3.5.1): authenticated Link File deep links + shared file request mode` + +**Commit message** + +```text +release(v3.5.1): authenticated Link File deep links + shared file request mode + +- files(core): add internal authenticated file links (create/resolve token endpoints + ACL-checked deep-link open) +- ui(files): add Link File in row context menu + toolbar single-selection action with copy-link modal +- boot/auth: preserve fileLink return URL through login/OIDC, resolve link post-auth, focus/highlight linked file +- shares: add upload-only File Request mode for shared folders with hide-listing support and dedicated drop-zone UI +- shared-upload: add per-link validation/rate limits/quota tracking + safer relative-path handling for chunked uploads +- admin: improve shared links listing with file-request grouping and created-by/source metadata +``` + +**Added** + +- **Authenticated internal file deep links (Core)** + - Added new API endpoints: + - `/api/file/createAuthFileLink.php` + - `/api/file/resolveAuthFileLink.php` + - Added internal token persistence/lookup for file links in `FileModel`. + - Added new frontend link module: + - `public/js/fileLink.js` +- **Shared folder file-request experience** + - Added upload-only (“drop”) mode support with hide-listing behavior. + - Added dedicated upload UI for drop mode: + - `public/js/sharedDropView.js` + - Added share options for upload constraints: + - max file size + - allowed file types + - daily file-count limit + - daily total-size limit + - preserve-folder-structure toggle + +**Changed** + +- **File actions/UI** + - Added `Link File` action to: + - file row context menu + - file toolbar (enabled only for exactly one selected file). +- **Auth/deep-link boot flow** + - `main.js` now preserves `?fileLink=` return URL through login/OIDC and resolves it after auth. + - Linked file navigation now opens the target folder and selects/highlights the file row. +- **Folder share modal + public share page** + - Share modal now supports both classic folder share and upload-only file request creation. + - Public shared-folder view switches between browse mode and drop mode assets/layout. +- **Admin shared-link management** + - Shared links list now distinguishes folder shares vs file requests and surfaces source/creator context. + +--- + ## Changes 02/16/2026 (v3.5.0) `release(v3.5.0): async transfer jobs + Transfer Center UX, service-layer API refactors, and folder UI consistency fixes` diff --git a/README.md b/README.md index 25d87de..de00c00 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,8 @@ Built for homelabs, teams, and client portals that need fast browsing, strict AC - 💾 **Self-hosted “cloud drive”** – Runs anywhere with PHP (or via Docker). No external database required. - 🔐 **Granular per-folder ACLs** – Manage View (all/own), Upload, Create, Edit, Rename, Move, Copy, Delete, Extract, Share, and more — all enforced consistently across the UI, API, and WebDAV. +- 🔗 **Link File (authenticated deep links)** – Generate internal links to specific files, require login + ACL checks, and open directly to the target in the app. +- 📥 **File Request links (upload-only)** – Share upload-only links so external users can submit files into a folder without browsing existing files. - 🔐 **Folder-level encryption at rest (optional)** – Encrypt entire folders (and all descendants) on disk using modern authenticated encryption. - Opt-in per folder with inherited protection for subfolders - Files are stored encrypted on disk and transparently decrypted on download diff --git a/public/api/file/createAuthFileLink.php b/public/api/file/createAuthFileLink.php new file mode 100644 index 0000000..8708026 --- /dev/null +++ b/public/api/file/createAuthFileLink.php @@ -0,0 +1,6 @@ +createAuthFileLink(); diff --git a/public/api/file/resolveAuthFileLink.php b/public/api/file/resolveAuthFileLink.php new file mode 100644 index 0000000..c00740d --- /dev/null +++ b/public/api/file/resolveAuthFileLink.php @@ -0,0 +1,6 @@ +resolveAuthFileLink(); diff --git a/public/api/folder/createShareFolderLink.php b/public/api/folder/createShareFolderLink.php index bf82bdd..de242eb 100644 --- a/public/api/folder/createShareFolderLink.php +++ b/public/api/folder/createShareFolderLink.php @@ -19,7 +19,15 @@ * @OA\Property(property="expirationUnit", type="string", enum={"seconds","minutes","hours","days"}, example="minutes"), * @OA\Property(property="password", type="string", example=""), * @OA\Property(property="allowUpload", type="integer", enum={0,1}, example=0), - * @OA\Property(property="allowSubfolders", type="integer", enum={0,1}, example=0) + * @OA\Property(property="allowSubfolders", type="integer", enum={0,1}, example=0), + * @OA\Property(property="mode", type="string", enum={"browse","drop"}, example="drop"), + * @OA\Property(property="fileDrop", type="integer", enum={0,1}, example=1), + * @OA\Property(property="hideListing", type="integer", enum={0,1}, example=1), + * @OA\Property(property="preserveFolderStructure", type="integer", enum={0,1}, example=1), + * @OA\Property(property="maxFileSizeMb", type="integer", example=100), + * @OA\Property(property="allowedTypes", type="array", @OA\Items(type="string", example="pdf")), + * @OA\Property(property="dailyFileLimit", type="integer", example=250), + * @OA\Property(property="maxTotalMbPerDay", type="integer", example=2048) * ) * ), * @OA\Response( diff --git a/public/api/folder/uploadToSharedFolder.php b/public/api/folder/uploadToSharedFolder.php index e31a885..4c7cebf 100644 --- a/public/api/folder/uploadToSharedFolder.php +++ b/public/api/folder/uploadToSharedFolder.php @@ -19,7 +19,14 @@ * @OA\Property(property="token", type="string", description="Share token"), * @OA\Property(property="pass", type="string", description="Share password (if required)"), * @OA\Property(property="path", type="string", description="Optional subfolder path within the shared folder"), - * @OA\Property(property="fileToUpload", type="string", format="binary", description="File to upload") + * @OA\Property(property="relativePath", type="string", description="Optional relative path for folder uploads"), + * @OA\Property(property="fileToUpload", type="string", format="binary", description="File to upload"), + * @OA\Property(property="resumableChunkNumber", type="integer", description="Chunk number for chunked upload"), + * @OA\Property(property="resumableTotalChunks", type="integer", description="Total chunks"), + * @OA\Property(property="resumableIdentifier", type="string", description="Chunk upload identifier"), + * @OA\Property(property="resumableFilename", type="string", description="Original file name"), + * @OA\Property(property="resumableRelativePath", type="string", description="Original relative path"), + * @OA\Property(property="file", type="string", format="binary", description="Chunk payload when chunked") * ) * ) * } diff --git a/public/css/share.css b/public/css/share.css index 1c24691..e6139d6 100644 --- a/public/css/share.css +++ b/public/css/share.css @@ -443,6 +443,12 @@ margin-bottom: 10px; } +.fr-share-upload-subtitle { + color: var(--share-muted); + font-size: 0.88rem; + margin: -4px 0 12px; +} + .fr-share-upload-form { display: flex; gap: 10px; @@ -454,6 +460,133 @@ flex: 1; } +.fr-share-upload-form-drop { + display: block; +} + +.fr-share-dropzone { + border: 1px dashed var(--share-border); + border-radius: 14px; + padding: 22px 14px; + text-align: center; + background: rgba(15, 23, 42, 0.02); + cursor: pointer; + transition: border-color 0.16s ease, background 0.16s ease, transform 0.16s ease; +} + +.fr-share-dropzone.is-dragover { + border-color: var(--share-accent); + background: rgba(37, 99, 235, 0.08); + transform: translateY(-1px); +} + +.fr-share-dropzone-title { + font-weight: 600; + font-size: 1rem; +} + +.fr-share-dropzone-subtitle { + font-size: 0.86rem; + color: var(--share-muted); + margin-top: 4px; +} + +.fr-share-dropzone-actions { + margin-top: 14px; + display: flex; + gap: 10px; + justify-content: center; + flex-wrap: wrap; +} + +.fr-share-drop-rules { + margin-top: 12px; + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.fr-share-rule-pill { + display: inline-flex; + align-items: center; + border: 1px solid var(--share-border); + border-radius: 999px; + padding: 4px 10px; + font-size: 0.76rem; + color: var(--share-muted); + background: rgba(15, 23, 42, 0.02); +} + +.fr-share-drop-queue { + margin-top: 12px; + display: flex; + flex-direction: column; + gap: 8px; +} + +.fr-share-queue-item { + border: 1px solid var(--share-border); + border-radius: 12px; + padding: 10px 12px; + background: rgba(15, 23, 42, 0.02); +} + +.fr-share-queue-item.is-done { + border-color: rgba(16, 185, 129, 0.45); +} + +.fr-share-queue-item.is-error { + border-color: rgba(220, 38, 38, 0.45); +} + +.fr-share-queue-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; +} + +.fr-share-queue-name { + font-size: 0.86rem; + font-weight: 500; + overflow-wrap: anywhere; +} + +.fr-share-queue-status { + font-size: 0.75rem; + color: var(--share-muted); + white-space: nowrap; +} + +.fr-share-queue-meta { + font-size: 0.72rem; + color: var(--share-muted); + margin-top: 4px; +} + +.fr-share-queue-progress { + margin-top: 8px; + height: 7px; + border-radius: 999px; + background: var(--share-border); + overflow: hidden; +} + +.fr-share-queue-progress-fill { + width: 0%; + height: 100%; + background: var(--share-accent); + transition: width 0.12s linear; +} + +.fr-share-queue-item.is-error .fr-share-queue-progress-fill { + background: #dc2626; +} + +.fr-share-queue-item.is-done .fr-share-queue-progress-fill { + background: #059669; +} + .fr-share-upload-progress { margin-top: 10px; display: flex; @@ -543,6 +676,10 @@ color: #b91c1c; } +.fr-share-upload-error { + margin-top: 10px; +} + .fr-share-breadcrumbs { display: flex; align-items: center; @@ -597,6 +734,13 @@ .fr-share-actions { width: 100%; } + .fr-share-dropzone-actions { + justify-content: stretch; + } + .fr-share-dropzone-actions .fr-share-btn { + flex: 1; + justify-content: center; + } .fr-share-preview iframe, .fr-share-preview img, .fr-share-preview video { diff --git a/public/css/styles.css b/public/css/styles.css index 1be4f3c..a87c0a7 100644 --- a/public/css/styles.css +++ b/public/css/styles.css @@ -1714,6 +1714,85 @@ label{font-size: 0.9rem;} transform: scale(1.05);} .share-modal-content{width: 600px !important; max-width: 90vw !important;} +.share-modal-body{display:flex; + flex-direction:column; + gap:12px;} +.share-modal-helper{margin:0; + color:#666; + font-size:.92em;} +.share-modal-section{border:1px solid #d9d9d9; + border-radius:10px; + padding:12px; + background:#fafafa;} +.share-section-title{margin:0 0 8px; + font-weight:600; + font-size:.96rem;} +.share-mode-toggle{display:grid; + grid-template-columns:repeat(2,minmax(0,1fr)); + gap:8px;} +.share-mode-btn{border:1px solid #cfcfcf; + background:#fff; + border-radius:10px; + padding:10px; + text-align:left; + cursor:pointer; + transition:border-color .15s ease,box-shadow .15s ease,background-color .15s ease;} +.share-mode-btn:hover{border-color:#0b5ed7;} +.share-mode-btn.is-active{border-color:#0b5ed7; + box-shadow:0 0 0 2px rgba(11,94,215,.15); + background:#f3f8ff;} +.share-mode-btn-title{display:block; + font-weight:600; + font-size:.93rem; + color:#111;} +.share-mode-btn-desc{display:block; + margin-top:4px; + font-size:.82rem; + color:#555; + line-height:1.3;} +.share-mode-notice{margin:8px 0 0; + font-size:.86rem; + color:#555;} +.share-field-label{display:block; + margin:0 0 6px; + font-size:.88rem; + font-weight:600;} +.share-field-input{width:100%; + padding:6px;} +.share-custom-expiration{margin-top:10px; + display:flex; + flex-wrap:wrap; + align-items:center; + gap:8px;} +.share-custom-expiration input[type="number"]{width:72px;} +.share-custom-expiration .share-warning{margin:0; + width:100%; + color:#a33; + font-size:.86rem;} +.share-check{display:flex; + align-items:flex-start; + gap:8px; + margin:0;} +.share-check + .share-check{margin-top:8px;} +.share-check-helper{margin:6px 0 0 25px; + font-size:.84rem; + color:#666;} +@media (max-width: 680px) { + .share-mode-toggle{grid-template-columns:1fr;} +} +.dark-mode .share-modal-helper{color:#b8b8b8;} +.dark-mode .share-modal-section{background:#262626; + border-color:#404040;} +.dark-mode .share-mode-btn{background:#1f1f1f; + border-color:#505050;} +.dark-mode .share-mode-btn:hover{border-color:#7ab3ff;} +.dark-mode .share-mode-btn.is-active{border-color:#7ab3ff; + box-shadow:0 0 0 2px rgba(122,179,255,.22); + background:#223042;} +.dark-mode .share-mode-btn-title{color:#ececec;} +.dark-mode .share-mode-btn-desc, +.dark-mode .share-mode-notice, +.dark-mode .share-check-helper{color:#b9b9b9;} .dark-mode .close-image-modal{background-color: rgba(0, 0, 0, 0.6); color: #ff6666;} .dark-mode .close-image-modal:hover{background-color: #ff6666; diff --git a/public/index.html b/public/index.html index b107b5d..348bf27 100644 --- a/public/index.html +++ b/public/index.html @@ -339,6 +339,9 @@ + @@ -639,6 +642,12 @@ sell Tag file + + `; + }); + + html += `
${esc(t("file_shares"))}