Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

[5.x] Drag and drop folders into the asset browser #10583

Merged
merged 16 commits into from
Oct 25, 2024
Merged
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
Loading