Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8d0477e0f7 | |||
| e7e9cb20d8 |
@@ -229,6 +229,8 @@ async function walkEntry(
|
|||||||
out.push({ file, rel: `${prefix}${entry.name}` });
|
out.push({ file, rel: `${prefix}${entry.name}` });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// The server takes nothing into .git: never read one off the disk at all.
|
||||||
|
if (entry.name === ".git") return;
|
||||||
const reader = (entry as FileSystemDirectoryEntry).createReader();
|
const reader = (entry as FileSystemDirectoryEntry).createReader();
|
||||||
// readEntries answers in pages until it answers [].
|
// readEntries answers in pages until it answers [].
|
||||||
for (;;) {
|
for (;;) {
|
||||||
@@ -614,8 +616,14 @@ export function FileBrowser({
|
|||||||
};
|
};
|
||||||
|
|
||||||
/** Upload `files` (each with its path inside the pick/drop) under `dir`. */
|
/** Upload `files` (each with its path inside the pick/drop) under `dir`. */
|
||||||
const uploadAll = async (files: { file: File; rel: string }[], dir = path) => {
|
const uploadAll = async (picked: { file: File; rel: string }[], dir = path) => {
|
||||||
setMenu(null);
|
setMenu(null);
|
||||||
|
// A dropped/picked repo carries its .git, which the server refuses file by
|
||||||
|
// file — skip it here with ONE notice instead of a toast per object.
|
||||||
|
const files = picked.filter((f) => !f.rel.split("/").includes(".git"));
|
||||||
|
if (files.length < picked.length) {
|
||||||
|
toast.info(t("fx.gitSkipped", { count: picked.length - files.length }));
|
||||||
|
}
|
||||||
if (!files.length) return;
|
if (!files.length) return;
|
||||||
const bytes = files.reduce((n, f) => n + f.file.size, 0);
|
const bytes = files.reduce((n, f) => n + f.file.size, 0);
|
||||||
if (bytes > FX_BATCH_MAX_BYTES) {
|
if (bytes > FX_BATCH_MAX_BYTES) {
|
||||||
|
|||||||
@@ -3,7 +3,8 @@
|
|||||||
* opens sub-folders, makes a folder inline, deletes into the Trash without a
|
* opens sub-folders, makes a folder inline, deletes into the Trash without a
|
||||||
* confirm, copies in one workspace and pastes into another (`fromLane`),
|
* confirm, copies in one workspace and pastes into another (`fromLane`),
|
||||||
* asks Replace / Keep both / Skip when an upload's name is taken, refuses a
|
* asks Replace / Keep both / Skip when an upload's name is taken, refuses a
|
||||||
* batch over 500 MB before sending anything, restores from the user's own
|
* batch over 500 MB before sending anything, leaves a repo's .git out of a
|
||||||
|
* folder upload with one notice, restores from the user's own
|
||||||
* Trash, opens an admin's Trash on one folder per user (counted, an unreadable
|
* Trash, opens an admin's Trash on one folder per user (counted, an unreadable
|
||||||
* one saying why), opens each user's Trash from its folder (asking before a
|
* one saying why), opens each user's Trash from its folder (asking before a
|
||||||
* purge there, sending the item's store) and
|
* purge there, sending the item's store) and
|
||||||
@@ -269,6 +270,23 @@ describe("FileExplorer", () => {
|
|||||||
expect(api.files.uploadBatch).not.toHaveBeenCalled();
|
expect(api.files.uploadBatch).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("leaves a picked repo's .git out of a folder upload, with one notice", async () => {
|
||||||
|
vi.mocked(api.files.upload).mockResolvedValue({ path: "x" });
|
||||||
|
open();
|
||||||
|
await screen.findByText("a.txt");
|
||||||
|
const mk = (rel: string) => {
|
||||||
|
const f = new File(["hi"], rel.split("/").pop()!);
|
||||||
|
Object.defineProperty(f, "webkitRelativePath", { value: rel });
|
||||||
|
return f;
|
||||||
|
};
|
||||||
|
const files = ["repo/.git/HEAD", "repo/.git/objects/d4/ab", "repo/src/a.js"].map(mk);
|
||||||
|
fireEvent.change(screen.getByTestId("fx-upload-folder"), { target: { files } });
|
||||||
|
await waitFor(() => expect(api.files.upload).toHaveBeenCalledTimes(1));
|
||||||
|
expect(vi.mocked(api.files.upload).mock.calls[0]![1]).toBe("repo/src/a.js");
|
||||||
|
expect(toast.info).toHaveBeenCalledTimes(1);
|
||||||
|
expect(toast.error).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
it("refuses a batch over 500 MB before sending anything", async () => {
|
it("refuses a batch over 500 MB before sending anything", async () => {
|
||||||
open();
|
open();
|
||||||
await screen.findByText("a.txt");
|
await screen.findByText("a.txt");
|
||||||
|
|||||||
@@ -590,6 +590,7 @@
|
|||||||
"fx.dropHint": "Drag files or folders here to upload · drag onto a folder to move (Ctrl: copy)",
|
"fx.dropHint": "Drag files or folders here to upload · drag onto a folder to move (Ctrl: copy)",
|
||||||
"fx.uploading": "Uploading {{done}}/{{total}}…",
|
"fx.uploading": "Uploading {{done}}/{{total}}…",
|
||||||
"fx.uploaded": "Uploaded {{count}} file(s).",
|
"fx.uploaded": "Uploaded {{count}} file(s).",
|
||||||
|
"fx.gitSkipped": "Skipped {{count}} file(s) inside .git — nothing is uploaded into .git.",
|
||||||
"fx.uploadFailed": "Could not upload {{name}}: {{message}}",
|
"fx.uploadFailed": "Could not upload {{name}}: {{message}}",
|
||||||
"fx.tooBig": "{{size}} is more than one upload may carry ({{max}}). Upload fewer files at a time.",
|
"fx.tooBig": "{{size}} is more than one upload may carry ({{max}}). Upload fewer files at a time.",
|
||||||
"fx.trashed": "Moved {{count}} item(s) to the Trash.",
|
"fx.trashed": "Moved {{count}} item(s) to the Trash.",
|
||||||
|
|||||||
@@ -590,6 +590,7 @@
|
|||||||
"fx.dropHint": "Kéo file hoặc thư mục vào đây để tải lên · kéo vào một thư mục để di chuyển (Ctrl: sao chép)",
|
"fx.dropHint": "Kéo file hoặc thư mục vào đây để tải lên · kéo vào một thư mục để di chuyển (Ctrl: sao chép)",
|
||||||
"fx.uploading": "Đang tải lên {{done}}/{{total}}…",
|
"fx.uploading": "Đang tải lên {{done}}/{{total}}…",
|
||||||
"fx.uploaded": "Đã tải lên {{count}} file.",
|
"fx.uploaded": "Đã tải lên {{count}} file.",
|
||||||
|
"fx.gitSkipped": "Đã bỏ qua {{count}} file trong .git — không tải gì vào .git.",
|
||||||
"fx.uploadFailed": "Không tải lên được {{name}}: {{message}}",
|
"fx.uploadFailed": "Không tải lên được {{name}}: {{message}}",
|
||||||
"fx.tooBig": "{{size}} vượt quá giới hạn một lần tải lên ({{max}}). Hãy tải ít file hơn mỗi lần.",
|
"fx.tooBig": "{{size}} vượt quá giới hạn một lần tải lên ({{max}}). Hãy tải ít file hơn mỗi lần.",
|
||||||
"fx.trashed": "Đã chuyển {{count}} mục vào Thùng rác.",
|
"fx.trashed": "Đã chuyển {{count}} mục vào Thùng rác.",
|
||||||
|
|||||||
@@ -7316,7 +7316,7 @@ exports[`screen snapshots > Settings — about 1`] = `
|
|||||||
<span
|
<span
|
||||||
class="text-status-warning/90"
|
class="text-status-warning/90"
|
||||||
>
|
>
|
||||||
UI build v2.8.47 (rebuild client to match)
|
UI build v2.8.48 (rebuild client to match)
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "agenthub",
|
"name": "agenthub",
|
||||||
"version": "2.8.47",
|
"version": "2.8.48",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "agenthub",
|
"name": "agenthub",
|
||||||
"version": "2.8.47",
|
"version": "2.8.48",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "UNLICENSED",
|
"license": "UNLICENSED",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "agenthub",
|
"name": "agenthub",
|
||||||
"version": "2.8.47",
|
"version": "2.8.48",
|
||||||
"bin": {
|
"bin": {
|
||||||
"agenthub": "bin/agenthub.js"
|
"agenthub": "bin/agenthub.js"
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user