Files
NetMesh/lib/uploadCompressed.ts
zhaolei 3c72efcb7f
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
[Init] Initial commit - NetMesh terminal manager
2026-09-13 18:24:01 +08:00

188 lines
7.5 KiB
TypeScript

import type { DropEntry } from "./sftpFileUtils";
import { getDropEntryLocalPath } from "./sftpFileUtils";
const getDropEntrySize = (entry: DropEntry): number => entry.file?.size ?? entry.size ?? 0;
import type { UploadCallbacks, UploadResult } from "./uploadService.types";
import type { UploadController } from "./uploadController";
const formatUploadError = (error: unknown): string =>
error instanceof Error ? error.message : String(error);
export async function uploadFoldersCompressed(
folderEntries: Array<[string, DropEntry[]]>,
targetPath: string,
sftpId: string,
callbacks?: UploadCallbacks,
controller?: UploadController
): Promise<UploadResult[]> {
const results: UploadResult[] = [];
// Import the compressed upload service
const { startCompressedUpload, checkCompressedUploadSupport } = await import('../infrastructure/services/compressUploadService');
for (const [folderName, entries] of folderEntries) {
if (controller?.isCancelled()) {
break;
}
// Prefer any file-like entry with a resolvable local path (native tree scans
// set localPath without a browser File handle).
const firstFile = entries.find((entry) => (
!entry.isDirectory && (!!entry.file || !!getDropEntryLocalPath(entry))
));
if (!firstFile) {
// Empty folder - mark for fallback to regular upload which will create the directory
results.push({ fileName: folderName, success: false, error: "Compressed upload not supported - fallback needed" });
continue;
}
const localFilePath = getDropEntryLocalPath(firstFile);
if (!localFilePath) {
results.push({ fileName: folderName, success: false, error: "Could not get local file path" });
continue;
}
// Extract folder path from the first file path
// Use DropEntry.relativePath which works for both file input and drag-drop scenarios
// For file input: webkitRelativePath is set (e.g., "folder/subdir/file.txt")
// For drag-drop: DropEntry.relativePath contains the correct path from extractDropEntries
const relativePath = firstFile.relativePath
|| (firstFile.file as (File & { webkitRelativePath?: string }) | null)?.webkitRelativePath
|| firstFile.file?.name
|| folderName;
// Normalize path separators for cross-platform compatibility
const normalizePathSeparators = (path: string) => path.replace(/\\/g, '/');
const normalizedLocalPath = normalizePathSeparators(localFilePath);
const normalizedRelativePath = normalizePathSeparators(relativePath);
// Calculate the root folder path by removing the full relativePath from localFilePath
// For example: if localFilePath is "/Users/rice/Downloads/110-temp/insideServer/subdir/file.txt"
// and relativePath is "insideServer/subdir/file.txt", we want "/Users/rice/Downloads/110-temp/insideServer"
let folderPath = localFilePath;
if (normalizedRelativePath && normalizedLocalPath.endsWith(normalizedRelativePath)) {
// Remove the relativePath from the end to get the base directory
const basePath = localFilePath.substring(0, localFilePath.length - relativePath.length);
// Remove trailing slash/backslash if present
const cleanBasePath = basePath.replace(/[/\\]$/, '');
// Add the folder name to get the actual folder path
folderPath = cleanBasePath + (cleanBasePath ? (localFilePath.includes('\\') ? '\\' : '/') : '') + folderName;
} else {
// Fallback: try to extract based on folder name with normalized separators
const normalizedFolderPattern1 = '/' + folderName + '/';
const normalizedFolderPattern2 = '\\' + folderName + '\\';
const folderIndex1 = normalizedLocalPath.lastIndexOf(normalizedFolderPattern1);
const folderIndex2 = localFilePath.lastIndexOf(normalizedFolderPattern2);
const folderIndex = Math.max(folderIndex1, folderIndex2);
if (folderIndex >= 0) {
folderPath = localFilePath.substring(0, folderIndex + folderName.length + 1);
} else {
// Last resort: remove just the filename (original logic)
const pathParts = normalizedRelativePath.split('/');
if (pathParts.length > 1) {
const fileName = pathParts[pathParts.length - 1];
if (normalizedLocalPath.endsWith(fileName)) {
folderPath = localFilePath.substring(0, localFilePath.length - fileName.length - 1);
}
} else {
// Single file, get its parent directory
const lastSlash = Math.max(localFilePath.lastIndexOf('/'), localFilePath.lastIndexOf('\\'));
if (lastSlash > 0) {
folderPath = localFilePath.substring(0, lastSlash);
}
}
}
}
let taskId: string | null = null; // Declare taskId outside try block for error handling
try {
// Check if compressed upload is supported
const support = await checkCompressedUploadSupport(sftpId);
if (!support.supported) {
// Fall back to regular upload for this folder
results.push({
fileName: folderName,
success: false,
error: "Compressed upload not supported - fallback needed"
});
continue;
}
const compressionId = crypto.randomUUID();
// Check for cancellation before starting
if (controller?.isCancelled()) {
results.push({ fileName: folderName, success: false, cancelled: true });
break;
}
// Register compression ID with controller for cancellation support
controller?.addActiveCompression(compressionId);
// Create a task for this folder compression
// Path-only drop entries (listLocalTree) carry size without a File handle.
const fileEntries = entries.filter((entry) => !entry.isDirectory);
const totalBytes = fileEntries.reduce((sum, entry) => sum + getDropEntrySize(entry), 0);
taskId = compressionId;
if (callbacks?.onTaskCreated) {
callbacks.onTaskCreated({
id: taskId,
fileName: folderName,
displayName: `${folderName} (compressed)`,
isDirectory: true,
progressMode: 'bytes',
totalBytes,
transferredBytes: 0,
speed: 0,
fileCount: fileEntries.length,
completedCount: 0,
sourcePath: folderPath,
controlKind: 'compressed-upload',
});
}
// Start compressed upload
const result = await startCompressedUpload(
{
compressionId,
folderPath,
targetPath,
sftpId,
folderName,
totalBytes,
},
);
controller?.removeActiveCompression(compressionId);
if (result.success) {
results.push({ fileName: folderName, success: true });
} else if (result.error?.includes('cancelled') || controller?.isCancelled()) {
// Handle cancellation
results.push({ fileName: folderName, success: false, cancelled: true });
} else {
results.push({ fileName: folderName, success: false, error: result.error });
}
} catch (error) {
const errorMessage = formatUploadError(error);
// Remove compression ID from controller on error
if (taskId) {
controller?.removeActiveCompression(taskId);
}
// Check if this was a cancellation
if (controller?.isCancelled() || errorMessage.includes('cancelled')) {
results.push({ fileName: folderName, success: false, cancelled: true });
} else {
results.push({ fileName: folderName, success: false, error: errorMessage });
}
}
}
return results;
}