Skip to content

Commit

Permalink
[5.x] Drag and drop folders into the asset browser (#10583)
Browse files Browse the repository at this point in the history
  • Loading branch information
daun authored Oct 25, 2024
1 parent c0e117a commit 4d08876
Show file tree
Hide file tree
Showing 4 changed files with 124 additions and 7 deletions.
67 changes: 61 additions & 6 deletions resources/js/components/assets/Uploader.vue
Original file line number Diff line number Diff line change
Expand Up @@ -103,11 +103,62 @@ export default {
e.preventDefault();
this.dragging = false;
for (let i = 0; i < e.dataTransfer.files.length; i++) {
this.addFile(e.dataTransfer.files[i]);
const { files, items } = e.dataTransfer;
// Handle DataTransferItems if browser supports dropping of folders
if (items && items.length && items[0].webkitGetAsEntry) {
this.addFilesFromDataTransferItems(items);
} else {
this.addFilesFromFileList(files);
}
},
addFilesFromFileList(files) {
for (let i = 0; i < files.length; i++) {
this.addFile(files[i]);
}
},
addFilesFromDataTransferItems(items) {
for (let i = 0; i < items.length; i++) {
let item = items[i];
if (item.webkitGetAsEntry) {
const entry = item.webkitGetAsEntry();
if (entry?.isFile) {
this.addFile(item.getAsFile());
} else if (entry?.isDirectory) {
this.addFilesFromDirectory(entry, entry.name);
}
} else if (item.getAsFile) {
if (item.kind === "file" || ! item.kind) {
this.addFile(item.getAsFile());
}
}
}
},
addFilesFromDirectory(directory, path) {
const reader = directory.createReader();
const readEntries = () => reader.readEntries((entries) => {
if (!entries.length) return;
for (let entry of entries) {
if (entry.isFile) {
entry.file((file) => {
if (! file.name.startsWith('.')) {
file.relativePath = path;
this.addFile(file);
}
});
} else if (entry.isDirectory) {
this.addFilesFromDirectory(entry, `${path}/${entry.name}`);
}
}
// Handle directories with more than 100 files in Chrome
readEntries();
}, console.error);
return readEntries();
},
addFile(file, data = {}) {
if (! this.enabled) return;
Expand Down Expand Up @@ -155,6 +206,11 @@ export default {
form.append('file', file);
// Pass along the relative path of files uploaded as a directory
if (file.relativePath) {
form.append('relativePath', file.relativePath);
}
let parameters = {
...this.extraData,
container: this.container,
Expand All @@ -174,11 +230,10 @@ export default {
},
processUploadQueue() {
const uploads = this.uploads.filter(u => !u.errorMessage);
if (uploads.length === 0) return;
// Make sure we're not grabbing a running or failed upload
const upload = this.uploads.find(u => u.instance.state === 'new' && !u.errorMessage);
if (!upload) return;
const upload = uploads[0];
const id = upload.id;
upload.instance.upload().then(response => {
Expand Down
9 changes: 9 additions & 0 deletions src/Assets/AssetUploader.php
Original file line number Diff line number Diff line change
Expand Up @@ -96,4 +96,13 @@ public static function getSafeFilename($string)
->when(config('statamic.assets.lowercase'), fn ($stringable) => $stringable->lower())
->ascii();
}

public static function getSafePath($path)
{
return Str::of($path)
->split('/[\/\\\\]+/')
->map(fn ($folder) => self::getSafeFilename($folder))
->filter()
->implode('/');
}
}
8 changes: 7 additions & 1 deletion src/Http/Controllers/CP/Assets/AssetsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -86,14 +86,20 @@ public function store(Request $request)
]);

$file = $request->file('file');
$folder = $request->folder;

// Append relative path as subfolder when upload was part of a folder and container allows it
if ($container->createFolders() && ($relativePath = AssetUploader::getSafePath($request->relativePath))) {
$folder = rtrim($folder, '/').'/'.$relativePath;
}

$basename = $request->option === 'rename' && $request->filename
? $request->filename.'.'.$file->getClientOriginalExtension()
: $file->getClientOriginalName();

$basename = AssetUploader::getSafeFilename($basename);

$path = ltrim($request->folder.'/'.$basename, '/');
$path = ltrim($folder.'/'.$basename, '/');

$validator = Validator::make(['path' => $path], ['path' => new UploadableAssetPath($container)]);

Expand Down
47 changes: 47 additions & 0 deletions tests/Feature/Assets/StoreAssetTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,53 @@ public function it_can_upload_with_different_filename()
$this->assertEquals(['path/to/newname.jpg', 'path/to/test.jpg'], $files);
}

#[Test]
public function it_can_upload_to_relative_path()
{
Storage::disk('test')->assertMissing('path/to/test.jpg');
Storage::disk('test')->assertMissing('path/to/sub/folder/test.jpg');

$this
->actingAs($this->userWithPermission())
->submit(['relativePath' => 'sub/folder'])
->assertOk()
->assertJson([
'data' => [
'id' => 'test_container::path/to/sub/folder/test.jpg',
'path' => 'path/to/sub/folder/test.jpg',
],
]);

Storage::disk('test')->assertMissing('path/to/test.jpg');
Storage::disk('test')->assertExists('path/to/sub/folder/test.jpg');
}

#[Test]
public function flattens_relative_path_unless_container_allows_creating_folders()
{
Storage::disk('test')->assertMissing('path/to/test.jpg');
Storage::disk('test')->assertMissing('path/to/sub/folder/test.jpg');

$createFolders = $this->container->createFolders();
$this->container->createFolders(false)->save();

$this
->actingAs($this->userWithPermission())
->submit(['relativePath' => 'sub/folder'])
->assertOk()
->assertJson([
'data' => [
'id' => 'test_container::path/to/test.jpg',
'path' => 'path/to/test.jpg',
],
]);

Storage::disk('test')->assertExists('path/to/test.jpg');
Storage::disk('test')->assertMissing('path/to/sub/folder/test.jpg');

$this->container->createFolders($createFolders)->save();
}

private function submit($overrides = [])
{
return $this->postJson(cp_route('assets.store'), $this->validPayload($overrides));
Expand Down

0 comments on commit 4d08876

Please sign in to comment.