diff --git a/app/Http/Controllers/SearchController.php b/app/Http/Controllers/SearchController.php index dc0be09..abdbde8 100644 --- a/app/Http/Controllers/SearchController.php +++ b/app/Http/Controllers/SearchController.php @@ -34,6 +34,7 @@ class SearchController extends Controller Transfer::query() ->where('user_id', $account->id) + ->shareTransfers() ->where('status', '!=', Transfer::STATUS_DELETED) ->where(function ($query) use ($like) { $query->where('title', 'like', $like) diff --git a/app/Http/Controllers/Transfer/FilesController.php b/app/Http/Controllers/Transfer/FilesController.php index 07e2db6..2aef591 100644 --- a/app/Http/Controllers/Transfer/FilesController.php +++ b/app/Http/Controllers/Transfer/FilesController.php @@ -48,7 +48,12 @@ class FilesController extends Controller ->where('user_id', $account->id) ->where('status', '!=', Transfer::STATUS_DELETED) ->when($folderTransfer, fn ($inner) => $inner->whereKey($folderTransfer->id), - fn ($inner) => $inner->where('is_folder', false))) + fn ($inner) => $inner->where(function ($query) { + $query->where('is_root_storage', true) + ->orWhere(function ($nested) { + $nested->where('is_folder', false)->where('is_root_storage', false); + }); + }))) ->when($search !== '', fn ($q) => $q->where('original_name', 'like', '%'.$search.'%')) ->with(['transfer.qrCode']) ->when($sort === 'oldest', fn ($q) => $q->oldest()) @@ -332,10 +337,11 @@ class FilesController extends Controller } $folder = $this->resolveUploadFolder($account, $data['folder'] ?? null); + $conflictLocation = $folder ?? $this->storage->rootStorage($account); $conflicts = $this->storage->findConflicts( $account, - $folder, + $conflictLocation, $data['filenames'], ); diff --git a/app/Http/Controllers/Transfer/OverviewController.php b/app/Http/Controllers/Transfer/OverviewController.php index ce71a14..05aa3bf 100644 --- a/app/Http/Controllers/Transfer/OverviewController.php +++ b/app/Http/Controllers/Transfer/OverviewController.php @@ -44,6 +44,7 @@ class OverviewController extends Controller $recentTransfers = Transfer::query() ->where('user_id', $account->id) + ->shareTransfers() ->accessible() ->with('qrCode') ->latest() diff --git a/app/Http/Controllers/Transfer/TransferController.php b/app/Http/Controllers/Transfer/TransferController.php index 6b85bc5..1c98199 100644 --- a/app/Http/Controllers/Transfer/TransferController.php +++ b/app/Http/Controllers/Transfer/TransferController.php @@ -34,6 +34,7 @@ class TransferController extends Controller $transfers = Transfer::query() ->where('user_id', $account->id) + ->shareTransfers() ->where('status', '!=', Transfer::STATUS_DELETED) ->when($search !== '', function ($query) use ($search) { $like = '%'.$search.'%'; @@ -207,5 +208,6 @@ class TransferController extends Controller private function authorizeTransfer(Transfer $transfer): void { abort_unless($transfer->user_id === ladill_account()->id, 403); + abort_if($transfer->isStorageContainer(), 404); } } diff --git a/app/Models/Transfer.php b/app/Models/Transfer.php index dda0d95..fd15b3f 100644 --- a/app/Models/Transfer.php +++ b/app/Models/Transfer.php @@ -166,6 +166,23 @@ class Transfer extends Model return $query->where('is_folder', true); } + public function scopeStorageContainers(Builder $query): Builder + { + return $query->where(function (Builder $inner) { + $inner->where('is_folder', true)->orWhere('is_root_storage', true); + }); + } + + public function scopeShareTransfers(Builder $query): Builder + { + return $query->where('is_folder', false)->where('is_root_storage', false); + } + + public function isShareTransfer(): bool + { + return ! $this->is_folder && ! $this->is_root_storage; + } + public function isStorageContainer(): bool { return $this->is_folder || $this->is_root_storage; diff --git a/app/Services/Transfer/FileStorageService.php b/app/Services/Transfer/FileStorageService.php index 1830d66..f3ef951 100644 --- a/app/Services/Transfer/FileStorageService.php +++ b/app/Services/Transfer/FileStorageService.php @@ -149,6 +149,7 @@ class FileStorageService DB::transaction(function () use ($user, $destination, $files, $resolutions, $maxBytes, &$uploaded) { $destination = Transfer::query()->lockForUpdate()->findOrFail($destination->id); $wasEmpty = $destination->files()->count() === 0; + $location = $destination->isStorageContainer() ? $destination : null; foreach ($files as $file) { if (! $file instanceof UploadedFile) { @@ -163,15 +164,15 @@ class FileStorageService $resolution = $resolutions[$originalName] ?? null; if ($resolution === 'replace') { - $existing = $this->findByName($user, $destination->is_folder ? $destination : null, $originalName); + $existing = $this->findByName($user, $location, $originalName); if ($existing) { $this->transfers->deleteFile($existing); } $storedName = $originalName; } elseif ($resolution === 'keep_both') { - $storedName = $this->uniqueFilename($user, $destination->is_folder ? $destination : null, $originalName); + $storedName = $this->uniqueFilename($user, $location, $originalName); } else { - $existing = $this->findByName($user, $destination->is_folder ? $destination : null, $originalName); + $existing = $this->findByName($user, $location, $originalName); if ($existing) { throw new RuntimeException("A file named \"{$originalName}\" already exists. Choose replace or keep both."); } @@ -205,7 +206,7 @@ class FileStorageService if ($folder !== null) { $query->whereKey($folder->id); } else { - $query->where('is_folder', false); + $query->where('is_folder', false)->where('is_root_storage', false); } }) ->first(); diff --git a/resources/js/app.js b/resources/js/app.js index f5cf728..fcabad2 100644 --- a/resources/js/app.js +++ b/resources/js/app.js @@ -312,6 +312,46 @@ function formatUploadBytes(bytes) { return `${bytes} B`; } +function uploadFileBasename(file) { + const relative = file.webkitRelativePath || ''; + if (relative.includes('/')) { + return relative.split('/').pop() || file.name; + } + + return (file.name.split(/[/\\]/).pop() || file.name); +} + +function flattenFolderUploadFiles(files) { + const usedNames = new Set(); + + return files.map((file) => { + let name = uploadFileBasename(file); + + if (usedNames.has(name)) { + const dot = name.lastIndexOf('.'); + const stem = dot > 0 ? name.slice(0, dot) : name; + const ext = dot > 0 ? name.slice(dot) : ''; + let counter = 1; + + do { + name = `${stem} (${counter})${ext}`; + counter += 1; + } while (usedNames.has(name)); + } + + usedNames.add(name); + + if (name === file.name && !file.webkitRelativePath?.includes('/')) { + return file; + } + + return new File([file], name, { + type: file.type, + lastModified: file.lastModified, + }); + }); +} + Alpine.data('transferCreateForm', (config = {}) => ({ fileItems: [], submitting: false, @@ -350,12 +390,7 @@ Alpine.data('transferCreateForm', (config = {}) => ({ return formatUploadBytes(bytes); }, - onFilesChange(event) { - if (this._ignoreInputChange) { - return; - } - - const picked = Array.from(event.target.files || []); + queueFilesForUpload(picked) { if (!picked.length) { return; } @@ -382,12 +417,32 @@ Alpine.data('transferCreateForm', (config = {}) => ({ }; this.fileItems.push(item); this._uploadQueue.push(item); + existingKeys.add(key); } - this.resetFileInput(event.target); void this.drainUploadQueue(); }, + onFilesChange(event) { + if (this._ignoreInputChange) { + return; + } + + const picked = Array.from(event.target.files || []); + this.resetFileInput(event.target); + this.queueFilesForUpload(picked); + }, + + onFolderChange(event) { + if (this._ignoreInputChange) { + return; + } + + const picked = flattenFolderUploadFiles(Array.from(event.target.files || [])); + this.resetFileInput(event.target); + this.queueFilesForUpload(picked); + }, + resetFileInput(input) { this._ignoreInputChange = true; input.value = ''; @@ -747,43 +802,11 @@ Alpine.data('filesManager', (config = {}) => ({ }, fileBasename(file) { - const relative = file.webkitRelativePath || ''; - if (relative.includes('/')) { - return relative.split('/').pop() || file.name; - } - - return (file.name.split(/[/\\]/).pop() || file.name); + return uploadFileBasename(file); }, flattenFolderUploadFiles(files) { - const usedNames = new Set(); - - return files.map((file) => { - let name = this.fileBasename(file); - - if (usedNames.has(name)) { - const dot = name.lastIndexOf('.'); - const stem = dot > 0 ? name.slice(0, dot) : name; - const ext = dot > 0 ? name.slice(dot) : ''; - let counter = 1; - - do { - name = `${stem} (${counter})${ext}`; - counter += 1; - } while (usedNames.has(name)); - } - - usedNames.add(name); - - if (name === file.name && !file.webkitRelativePath?.includes('/')) { - return file; - } - - return new File([file], name, { - type: file.type, - lastModified: file.lastModified, - }); - }); + return flattenFolderUploadFiles(files); }, cancelUploadConfirm() { diff --git a/resources/views/transfer/transfers/create.blade.php b/resources/views/transfer/transfers/create.blade.php index 360590f..f741905 100644 --- a/resources/views/transfer/transfers/create.blade.php +++ b/resources/views/transfer/transfers/create.blade.php @@ -35,10 +35,22 @@
Up to {{ $maxFiles }} files. Uploads start immediately — pick more files anytime before creating the transfer.
+ Files +Up to {{ $maxFiles }} files. Uploads start immediately — pick files or a folder anytime before creating the transfer.