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
113 lines
4.1 KiB
TypeScript
113 lines
4.1 KiB
TypeScript
import { useCallback } from "react";
|
|
import type { SftpFilenameEncoding, TransferTask } from "../../../domain/models";
|
|
import { netcattyBridge } from "../../../infrastructure/services/netcattyBridge";
|
|
import { isMissingStatError } from "./errors";
|
|
import type { SftpPane } from "./types";
|
|
import { getParentPath, joinPath } from "./utils";
|
|
|
|
export function useSftpTransferConflictOps() {
|
|
const splitNameForDuplicate = useCallback((fileName: string, isDirectory: boolean) => {
|
|
if (isDirectory) return { baseName: fileName, ext: "" };
|
|
const lastDot = fileName.lastIndexOf(".");
|
|
if (lastDot <= 0) return { baseName: fileName, ext: "" };
|
|
return {
|
|
baseName: fileName.slice(0, lastDot),
|
|
ext: fileName.slice(lastDot),
|
|
};
|
|
}, []);
|
|
|
|
const statTargetPath = useCallback(
|
|
async (
|
|
targetPane: SftpPane,
|
|
targetSftpId: string | null,
|
|
targetPath: string,
|
|
targetEncoding: SftpFilenameEncoding,
|
|
): Promise<{ type?: "file" | "directory" | "symlink"; size: number; mtime: number } | null> => {
|
|
if (!targetPane.connection) return null;
|
|
|
|
try {
|
|
if (targetPane.connection.isLocal) {
|
|
const bridge = netcattyBridge.get();
|
|
const stat = await (bridge?.lstatLocal ?? bridge?.statLocal)?.(targetPath);
|
|
if (!stat) return null;
|
|
return {
|
|
type: stat.type as "file" | "directory" | "symlink" | undefined,
|
|
size: stat.size,
|
|
mtime: stat.lastModified || Date.now(),
|
|
};
|
|
}
|
|
|
|
if (!targetSftpId) return null;
|
|
const bridge = netcattyBridge.get();
|
|
const stat = await (bridge?.lstatSftp ?? bridge?.statSftp)?.(
|
|
targetSftpId,
|
|
targetPath,
|
|
targetEncoding,
|
|
);
|
|
if (!stat) return null;
|
|
return {
|
|
type: stat.type as "file" | "directory" | "symlink" | undefined,
|
|
size: stat.size,
|
|
mtime: stat.lastModified || Date.now(),
|
|
};
|
|
} catch (error) {
|
|
// Missing path = no conflict. ENOTSUP / unknown type must fail closed.
|
|
if (isMissingStatError(error)) return null;
|
|
throw error;
|
|
}
|
|
},
|
|
[],
|
|
);
|
|
|
|
const getDuplicateTarget = useCallback(
|
|
async (
|
|
task: TransferTask,
|
|
targetPane: SftpPane,
|
|
targetSftpId: string | null,
|
|
targetEncoding: SftpFilenameEncoding,
|
|
) => {
|
|
const parentPath = getParentPath(task.targetPath);
|
|
const { baseName, ext } = splitNameForDuplicate(task.fileName, task.isDirectory);
|
|
|
|
for (let index = 1; index < 1000; index++) {
|
|
const suffix = index === 1 ? " (copy)" : ` (copy ${index})`;
|
|
const fileName = `${baseName}${suffix}${ext}`;
|
|
const targetPath = joinPath(parentPath, fileName);
|
|
// Unsupported LSTAT must propagate — do not treat as a free name.
|
|
const existing = await statTargetPath(targetPane, targetSftpId, targetPath, targetEncoding);
|
|
if (!existing) return { fileName, targetPath };
|
|
}
|
|
|
|
const fallbackName = `${baseName} (copy ${Date.now()})${ext}`;
|
|
return { fileName: fallbackName, targetPath: joinPath(parentPath, fallbackName) };
|
|
},
|
|
[splitNameForDuplicate, statTargetPath],
|
|
);
|
|
|
|
const deleteTargetPath = useCallback(
|
|
async (
|
|
task: TransferTask,
|
|
targetPane: SftpPane,
|
|
targetSftpId: string | null,
|
|
targetEncoding: SftpFilenameEncoding,
|
|
expectedType?: "file" | "directory" | "symlink",
|
|
) => {
|
|
if (!targetPane.connection) return;
|
|
if (targetPane.connection.isLocal) {
|
|
const deleteLocalFile = netcattyBridge.get()?.deleteLocalFile;
|
|
if (!deleteLocalFile) throw new Error("Local delete unavailable");
|
|
await deleteLocalFile(task.targetPath, expectedType);
|
|
return;
|
|
}
|
|
if (!targetSftpId) throw new Error("Target SFTP session not found");
|
|
const deleteSftp = netcattyBridge.get()?.deleteSftp;
|
|
if (!deleteSftp) throw new Error("SFTP delete unavailable");
|
|
await deleteSftp(targetSftpId, task.targetPath, targetEncoding, expectedType);
|
|
},
|
|
[],
|
|
);
|
|
|
|
|
|
return { statTargetPath, getDuplicateTarget, deleteTargetPath };
|
|
}
|