Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Unverified Commit 9b38c940 authored by Simon Chan's avatar Simon Chan
Browse files

feat(demo): download multiple files/folders as zip

fixes #518
parent 92364b1a
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -17,6 +17,7 @@
        "DESERIALIZERS",
        "ebml",
        "Embedder",
        "fflate",
        "fluentui",
        "genymobile",
        "Genymobile's",
+1 −0
Original line number Diff line number Diff line
@@ -32,6 +32,7 @@
        "@yume-chan/stream-extra": "workspace:^0.0.18",
        "@yume-chan/stream-saver": "^2.0.6",
        "@yume-chan/struct": "workspace:^0.0.18",
        "fflate": "^0.7.4",
        "mobx": "^6.7.0",
        "mobx-react-lite": "^3.4.3",
        "next": "13.2.4",
+179 −53
Original line number Diff line number Diff line
@@ -29,8 +29,15 @@ import {
} from "@fluentui/react-file-type-icons";
import { useConst } from "@fluentui/react-hooks";
import { getIcon } from "@fluentui/style-utilities";
import { AdbFeature, LinuxFileType, type AdbSyncEntry } from "@yume-chan/adb";
import { WrapConsumableStream } from "@yume-chan/stream-extra";
import {
    AdbFeature,
    AdbSync,
    LinuxFileType,
    type AdbSyncEntry,
} from "@yume-chan/adb";
import { WrapConsumableStream, WritableStream } from "@yume-chan/stream-extra";
import { EMPTY_UINT8_ARRAY } from "@yume-chan/struct";
import { Zip, ZipPassThrough } from "fflate";
import {
    action,
    autorun,
@@ -174,9 +181,9 @@ class FileManagerState {
                    },
                });
                break;
            case 1:
                if (this.selectedItems[0].type === LinuxFileType.File) {
                    result.push({
            default:
                result.push(
                    {
                        key: "download",
                        text: "Download",
                        iconProps: {
@@ -188,40 +195,20 @@ class FileManagerState {
                            },
                        },
                        onClick: () => {
                            (async () => {
                                const sync = await GLOBAL_STATE.device!.sync();
                                try {
                                    const item = this.selectedItems[0];
                                    const itemPath = path.resolve(
                                        this.path,
                                        item.name
                                    );
                                    await sync
                                        .read(itemPath)
                                        .pipeTo(
                                            saveFile(
                                                item.name,
                                                Number(item.size)
                                            )
                                        );
                                } catch (e: any) {
                                    GLOBAL_STATE.showErrorDialog(e);
                                } finally {
                                    sync.dispose();
                                }
                            })();
                            void this.download();
                            return false;
                        },
                    });
                }
            // fall through
            default:
                result.push({
                    },
                    {
                        key: "delete",
                        text: "Delete",
                        iconProps: {
                            iconName: Icons.Delete,
                        style: { height: 20, fontSize: 20, lineHeight: 1.5 },
                            style: {
                                height: 20,
                                fontSize: 20,
                                lineHeight: 1.5,
                            },
                        },
                        onClick: () => {
                            (async () => {
@@ -229,10 +216,15 @@ class FileManagerState {
                                    for (const item of this.selectedItems) {
                                        const output =
                                            await GLOBAL_STATE.device!.rm(
                                            path.resolve(this.path, item.name!)
                                                path.resolve(
                                                    this.path,
                                                    item.name!
                                                )
                                            );
                                        if (output) {
                                        GLOBAL_STATE.showErrorDialog(output);
                                            GLOBAL_STATE.showErrorDialog(
                                                output
                                            );
                                            return;
                                        }
                                    }
@@ -244,7 +236,8 @@ class FileManagerState {
                            })();
                            return false;
                        },
                });
                    }
                );
                break;
        }

@@ -459,6 +452,139 @@ class FileManagerState {
        });
    }

    private getFileStream(sync: AdbSync, basePath: string, name: string) {
        return sync.read(path.resolve(basePath, name));
    }

    private async addDirectory(
        sync: AdbSync,
        zip: Zip,
        basePath: string,
        relativePath: string
    ) {
        if (relativePath !== ".") {
            // Add empty directory
            const file = new ZipPassThrough(relativePath + "/");
            zip.add(file);
            file.push(EMPTY_UINT8_ARRAY, true);
        }

        for (const entry of await sync.readdir(
            path.resolve(basePath, relativePath)
        )) {
            if (entry.name === "." || entry.name === "..") {
                continue;
            }

            switch (entry.type) {
                case LinuxFileType.Directory:
                    await this.addDirectory(
                        sync,
                        zip,
                        basePath,
                        path.resolve(relativePath, entry.name)
                    );
                    break;
                case LinuxFileType.File:
                    await this.addFile(
                        sync,
                        zip,
                        basePath,
                        path.resolve(relativePath, entry.name)
                    );
                    break;
            }
        }
    }

    private async addFile(
        sync: AdbSync,
        zip: Zip,
        basePath: string,
        name: string
    ) {
        const file = new ZipPassThrough(name);
        zip.add(file);
        await this.getFileStream(sync, basePath, name).pipeTo(
            new WritableStream({
                write(chunk) {
                    file.push(chunk);
                },
                close() {
                    file.push(EMPTY_UINT8_ARRAY, true);
                },
            })
        );
    }

    private async download() {
        const sync = await GLOBAL_STATE.device!.sync();
        try {
            if (this.selectedItems.length === 1) {
                const item = this.selectedItems[0];
                switch (item.type) {
                    case LinuxFileType.Directory: {
                        const stream = saveFile(
                            `${this.selectedItems[0].name}.zip`
                        );
                        const writer = stream.getWriter();
                        const zip = new Zip((err, data, final) => {
                            writer.write(data);
                            if (final) {
                                writer.close();
                            }
                        });
                        await this.addDirectory(
                            sync,
                            zip,
                            path.resolve(this.path, item.name),
                            "."
                        );
                        zip.end();
                        break;
                    }
                    case LinuxFileType.File:
                        await this.getFileStream(
                            sync,
                            this.path,
                            item.name
                        ).pipeTo(saveFile(item.name, Number(item.size)));
                        break;
                }
                return;
            }

            const stream = saveFile(`${path.basename(this.path)}.zip`);
            const writer = stream.getWriter();
            const zip = new Zip((err, data, final) => {
                writer.write(data);
                if (final) {
                    writer.close();
                }
            });
            for (const item of this.selectedItems) {
                switch (item.type) {
                    case LinuxFileType.Directory:
                        await this.addDirectory(
                            sync,
                            zip,
                            this.path,
                            item.name
                        );
                        break;
                    case LinuxFileType.File:
                        await this.addFile(sync, zip, this.path, item.name);
                        break;
                }
            }
            zip.end();
        } catch (e: any) {
            GLOBAL_STATE.showErrorDialog(e);
        } finally {
            sync.dispose();
        }
    }

    pushPathQuery = (path: string) => {
        Router.push({ query: { ...Router.query, path } });
    };
+6 −0
Original line number Diff line number Diff line
@@ -37,6 +37,7 @@ importers:
      '@yume-chan/struct': workspace:^0.0.18
      eslint: ^8.36.0
      eslint-config-next: 13.2.4
      fflate: ^0.7.4
      mobx: ^6.7.0
      mobx-react-lite: ^3.4.3
      next: 13.2.4
@@ -73,6 +74,7 @@ importers:
      '@yume-chan/stream-extra': link:../../libraries/stream-extra
      '@yume-chan/stream-saver': 2.0.6
      '@yume-chan/struct': link:../../libraries/struct
      fflate: 0.7.4
      mobx: 6.8.0
      mobx-react-lite: 3.4.3_woojb62cqeyk443mbl7msrwu2e
      next: 13.2.4_biqbaboplfbrettd7655fr4n2y
@@ -4325,6 +4327,10 @@ packages:
      pend: 1.2.0
    dev: true

  /fflate/0.7.4:
    resolution: {integrity: sha512-5u2V/CDW15QM1XbbgS+0DfPxVB+jUKhWEKuuFuHncbk3tEEqzmoXL+2KyOFuKGqOnmdIy0/davWF1CkuwtibCw==}
    dev: false

  /file-entry-cache/6.0.1:
    resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==}
    engines: {node: ^10.12.0 || >=12.0.0}
+1 −1
Original line number Diff line number Diff line
// DO NOT MODIFY THIS FILE MANUALLY BUT DO COMMIT IT. It is generated and used by Rush.
{
  "pnpmShrinkwrapHash": "0fcefb11b89cf49ba2dbecc264e8ee43ce30d539",
  "pnpmShrinkwrapHash": "c04ca9a2ee7db66f447f8fb3e6277592fe5b45e7",
  "preferredVersionsHash": "bf21a9e8fbc5a3846fb05b4fa0859e0917b2202f"
}
Loading