quickly upload files to a remote server via rsync
at main 85 lines 2.3 kB view raw
1import { spawn } from "child_process"; 2import { join } from "path"; 3import { homedir } from "os"; 4import { DEFAULTS_TIMEOUT_MS } from "./constants"; 5 6export type Result<T, E> = 7 | { ok: true; value: T } 8 | { ok: false; error: E }; 9 10export function formatTimestamp(date: Date): string { 11 const year = date.getFullYear(); 12 const month = String(date.getMonth() + 1).padStart(2, "0"); 13 const day = String(date.getDate()).padStart(2, "0"); 14 const hours = String(date.getHours()).padStart(2, "0"); 15 const minutes = String(date.getMinutes()).padStart(2, "0"); 16 const seconds = String(date.getSeconds()).padStart(2, "0"); 17 18 return `${year}-${month}-${day} at ${hours}.${minutes}.${seconds}`; 19} 20 21export type FilenameValidationError = 22 | "empty" 23 | "too_long" 24 | "path_traversal" 25 | "invalid_chars"; 26 27export function validateFilename( 28 filename: string 29): Result<string, FilenameValidationError> { 30 if (!filename || filename.trim() === "") { 31 return { ok: false, error: "empty" }; 32 } 33 34 if (filename.length > 255) { 35 return { ok: false, error: "too_long" }; 36 } 37 38 // Check for path traversal attempts 39 if ( 40 filename.includes("..") || 41 filename.includes("/") || 42 filename.includes("\\") 43 ) { 44 return { ok: false, error: "path_traversal" }; 45 } 46 47 // Check for null bytes and control characters 48 if (/[\x00-\x1f\x7f-\x9f]/.test(filename)) { 49 return { ok: false, error: "invalid_chars" }; 50 } 51 52 return { ok: true, value: filename.trim() }; 53} 54 55export async function getCaptureDirectory(): Promise<string> { 56 return new Promise((resolve) => { 57 const proc = spawn("defaults", ["read", "com.apple.screencapture", "location"]); 58 let output = ""; 59 let timeoutId: Timer | null = null; 60 61 proc.stdout.on("data", (data) => { 62 output += data.toString(); 63 }); 64 65 proc.on("close", (code) => { 66 if (timeoutId) clearTimeout(timeoutId); 67 if (code === 0 && output.trim()) { 68 resolve(output.trim()); 69 } else { 70 resolve(join(homedir(), "Desktop")); 71 } 72 }); 73 74 proc.on("error", () => { 75 if (timeoutId) clearTimeout(timeoutId); 76 resolve(join(homedir(), "Desktop")); 77 }); 78 79 // Add timeout for defaults command 80 timeoutId = setTimeout(() => { 81 proc.kill("SIGTERM"); 82 resolve(join(homedir(), "Desktop")); 83 }, DEFAULTS_TIMEOUT_MS); 84 }); 85}