Skip to content

Commit 7a39dff

Browse files
committed
Preserve resource forks when dragging in folders
We can use the webkitGetAsEntry API to iterate over directories, and in addition to uploading the base file, also upload its resource fork by trying to read the ..namedfork/rsrc parallel file for it. This only works in Chrome (Safari returns the File object, but trying to read it fails, and Firefox doesn't even return the File object). It also doesn't handle metadata like creator/type codes, so the files are not directly usable even in Chrome. But it's sort of cool that it works at all. Updates #255
1 parent a3ca7be commit 7a39dff

2 files changed

Lines changed: 128 additions & 35 deletions

File tree

‎src/Mac.tsx‎

Lines changed: 107 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -372,10 +372,23 @@ export default function Mac({
372372
function handleDrop(event: React.DragEvent) {
373373
event.preventDefault();
374374
setDragCount(0);
375+
const emulator = emulatorRef.current;
376+
if (!emulator) {
377+
return;
378+
}
375379
const files = [];
376380

377381
if (event.dataTransfer.items) {
378382
for (const item of event.dataTransfer.items) {
383+
const entry = item.webkitGetAsEntry?.();
384+
if (entry?.isDirectory) {
385+
uploadDirectory(
386+
emulator,
387+
entry as FileSystemDirectoryEntry
388+
);
389+
return;
390+
}
391+
379392
if (item.kind === "file") {
380393
files.push(item.getAsFile()!);
381394
} else if (
@@ -402,7 +415,7 @@ export default function Mac({
402415
}
403416
}
404417

405-
uploadFiles(files);
418+
uploadFiles(emulator, files);
406419
}
407420

408421
function handleLoadFileClick() {
@@ -415,8 +428,8 @@ export default function Mac({
415428
// the synthetic click event below) because we may not get a change
416429
// event if the user cancels the file picker.
417430
setDragCount(1);
418-
if (input.files) {
419-
uploadFiles(Array.from(input.files));
431+
if (input.files && emulatorRef.current) {
432+
uploadFiles(emulatorRef.current, Array.from(input.files));
420433
}
421434
input.remove();
422435
// Delay removing the overlay a bit so that users have a chance to
@@ -426,29 +439,6 @@ export default function Mac({
426439
input.click();
427440
}
428441

429-
function uploadFiles(files: File[]) {
430-
const emulator = emulatorRef.current;
431-
if (!emulator) {
432-
return;
433-
}
434-
435-
let fileCount = 0;
436-
let diskImageCount = 0;
437-
for (const file of files) {
438-
if (isDiskImageFile(file.name)) {
439-
diskImageCount++;
440-
} else {
441-
fileCount++;
442-
}
443-
emulator.uploadFile(file);
444-
}
445-
varz.incrementMulti({
446-
"emulator_uploads": files.length,
447-
"emulator_uploads:files": fileCount,
448-
"emulator_uploads:disks": diskImageCount,
449-
});
450-
}
451-
452442
function loadCDROM(cdrom: EmulatorCDROM) {
453443
varz.incrementMulti({
454444
"emulator_cdroms": 1,
@@ -595,6 +585,97 @@ export default function Mac({
595585
);
596586
}
597587

588+
/**
589+
* Handles a full directory upload, including attempting to read resource forks
590+
* for files that have them.
591+
*/
592+
function uploadDirectory(
593+
emulator: Emulator,
594+
directoryEntry: FileSystemDirectoryEntry
595+
) {
596+
let inProgressCount = 1; // we're waiting to read the starting directory
597+
const files: File[] = [];
598+
const names: string[] = [];
599+
const handleFile = (file: File, path: string[]) => {
600+
files.push(file);
601+
names.push(path.join("/"));
602+
checkDone();
603+
};
604+
605+
const checkDone = () => {
606+
inProgressCount--;
607+
if (inProgressCount === 0) {
608+
uploadFiles(emulator, files, names);
609+
}
610+
};
611+
612+
const readDirectory = (
613+
directoryEntry: FileSystemDirectoryEntry,
614+
path: string[]
615+
) => {
616+
const directoryReader = directoryEntry.createReader();
617+
directoryReader.readEntries(entries => {
618+
inProgressCount--; // We finished reading the directory
619+
entries.forEach(entry => {
620+
inProgressCount++; //
621+
if (entry.isDirectory) {
622+
readDirectory(entry as FileSystemDirectoryEntry, [
623+
...path,
624+
entry.name,
625+
]);
626+
return;
627+
}
628+
const fileEntry = entry as FileSystemFileEntry;
629+
fileEntry.file(file => handleFile(file, [...path, entry.name]));
630+
631+
inProgressCount++;
632+
directoryEntry.getFile(
633+
`${entry.name}/..namedfork/rsrc`,
634+
{},
635+
resourceEntry =>
636+
(resourceEntry as FileSystemFileEntry).file(
637+
resourceFile =>
638+
// Assume that the emulator is using the
639+
// Basilisk II/SheepShaver ExtFS convention of a
640+
// parallel .rsrc directory for resource forks.
641+
handleFile(resourceFile, [
642+
...path,
643+
".rsrc",
644+
entry.name,
645+
]),
646+
checkDone // Ignore errors, not all files have resource forks
647+
),
648+
checkDone // Ignore errors, not all files have resource forks
649+
);
650+
});
651+
});
652+
};
653+
654+
readDirectory(directoryEntry, [directoryEntry.name]);
655+
}
656+
657+
function uploadFiles(emulator: Emulator, files: File[], names: string[] = []) {
658+
let fileCount = 0;
659+
let diskImageCount = 0;
660+
for (const file of files) {
661+
if (isDiskImageFile(file.name)) {
662+
diskImageCount++;
663+
} else {
664+
fileCount++;
665+
}
666+
}
667+
const resourceForkCount = names.filter(name =>
668+
name.includes("/.rsrc/")
669+
).length;
670+
emulator.uploadFiles(files, names);
671+
varz.incrementMulti({
672+
"emulator_uploads": files.length,
673+
"emulator_uploads:files": fileCount,
674+
"emulator_uploads:disks": diskImageCount,
675+
"emulator_uploads:resource_forks": resourceForkCount,
676+
});
677+
}
678+
598679
const SMALL_BEZEL_THRESHOLD = 80;
599680
const MEDIUM_BEZEL_THRESHOLD = 168;
600681

‎src/emulator/emulator-ui.ts‎

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -428,17 +428,29 @@ export class Emulator {
428428
});
429429
}
430430

431-
async uploadFile(file: File) {
432-
const uploads = await uploadsFromDirectoryExtractionFile(file);
433-
if (uploads) {
434-
this.#files.uploadFiles(uploads);
431+
async uploadFiles(files: File[], names: string[] = []) {
432+
const remainingFiles = [];
433+
const remainingNames: string[] = [];
434+
for (let i = 0; i < files.length; i++) {
435+
const file = files[i];
436+
const uploads = await uploadsFromDirectoryExtractionFile(file);
437+
if (uploads) {
438+
this.#files.uploadFiles(uploads);
439+
continue;
440+
}
441+
remainingFiles.push(file);
442+
remainingNames.push(names[i]);
443+
}
444+
if (!remainingFiles.length) {
435445
return;
436446
}
437-
this.#files.uploadFile({
438-
name: file.name,
439-
url: URL.createObjectURL(file),
440-
size: file.size,
441-
});
447+
this.#files.uploadFiles(
448+
remainingFiles.map((file, i) => ({
449+
name: remainingNames[i] ?? file.name, // Full paths are handled by worker
450+
url: URL.createObjectURL(file),
451+
size: file.size,
452+
}))
453+
);
442454
}
443455

444456
loadCDROM(cdrom: EmulatorCDROM) {

0 commit comments

Comments
 (0)