quickly upload files to a remote server via rsync
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}