@seanmozeik/s3up 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +204 -0
- package/package.json +55 -0
- package/src/commands/list.test.ts +31 -0
- package/src/commands/list.ts +58 -0
- package/src/commands/prune.test.ts +83 -0
- package/src/commands/prune.ts +159 -0
- package/src/commands/upload.ts +350 -0
- package/src/index.test.ts +51 -0
- package/src/index.ts +165 -0
- package/src/lib/archive.test.ts +65 -0
- package/src/lib/archive.ts +89 -0
- package/src/lib/clipboard.ts +41 -0
- package/src/lib/flags.test.ts +72 -0
- package/src/lib/flags.ts +129 -0
- package/src/lib/multipart.ts +507 -0
- package/src/lib/output.test.ts +74 -0
- package/src/lib/output.ts +63 -0
- package/src/lib/progress-bar.ts +155 -0
- package/src/lib/providers.ts +150 -0
- package/src/lib/s3.test.ts +42 -0
- package/src/lib/s3.ts +124 -0
- package/src/lib/secrets.ts +81 -0
- package/src/lib/signing.ts +151 -0
- package/src/lib/state.ts +145 -0
- package/src/lib/upload.ts +120 -0
- package/src/ui/banner.ts +47 -0
- package/src/ui/setup.ts +161 -0
- package/src/ui/theme.ts +140 -0
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// src/lib/archive.ts
|
|
2
|
+
|
|
3
|
+
import { readdir, stat } from 'node:fs/promises';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
|
|
6
|
+
export interface ArchiveOptions {
|
|
7
|
+
compression: number;
|
|
8
|
+
name?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface ArchiveResult {
|
|
12
|
+
blob: Blob;
|
|
13
|
+
name: string;
|
|
14
|
+
size: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function generateArchiveName(directories: string[], override?: string): string {
|
|
18
|
+
if (override) {
|
|
19
|
+
return override.endsWith('.tar.gz') ? override : `${override}.tar.gz`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const firstName = path.basename(directories[0]);
|
|
23
|
+
const date = new Date().toISOString().split('T')[0];
|
|
24
|
+
return `${firstName}-${date}.tar.gz`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function collectFiles(
|
|
28
|
+
dirPath: string,
|
|
29
|
+
baseName: string
|
|
30
|
+
): Promise<Record<string, Uint8Array>> {
|
|
31
|
+
const files: Record<string, Uint8Array> = {};
|
|
32
|
+
|
|
33
|
+
async function walk(currentPath: string, relativePath: string): Promise<void> {
|
|
34
|
+
const entries = await readdir(currentPath, { withFileTypes: true });
|
|
35
|
+
|
|
36
|
+
for (const entry of entries) {
|
|
37
|
+
const fullPath = path.join(currentPath, entry.name);
|
|
38
|
+
const archivePath = path.join(relativePath, entry.name);
|
|
39
|
+
|
|
40
|
+
if (entry.isDirectory()) {
|
|
41
|
+
await walk(fullPath, archivePath);
|
|
42
|
+
} else if (entry.isFile()) {
|
|
43
|
+
const content = await Bun.file(fullPath).bytes();
|
|
44
|
+
files[archivePath] = content;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
await walk(dirPath, baseName);
|
|
50
|
+
return files;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function createArchive(
|
|
54
|
+
directories: string[],
|
|
55
|
+
options: ArchiveOptions
|
|
56
|
+
): Promise<ArchiveResult> {
|
|
57
|
+
const allFiles: Record<string, Uint8Array> = {};
|
|
58
|
+
|
|
59
|
+
// Collect files from all directories
|
|
60
|
+
for (const dir of directories) {
|
|
61
|
+
const baseName = path.basename(dir);
|
|
62
|
+
const dirFiles = await collectFiles(dir, baseName);
|
|
63
|
+
Object.assign(allFiles, dirFiles);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Create tarball using Bun.Archive
|
|
67
|
+
const archive = new Bun.Archive(allFiles, {
|
|
68
|
+
compress: 'gzip',
|
|
69
|
+
level: options.compression
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const blob = await archive.blob();
|
|
73
|
+
const name = generateArchiveName(directories, options.name);
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
blob,
|
|
77
|
+
name,
|
|
78
|
+
size: blob.size
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export async function isDirectory(filePath: string): Promise<boolean> {
|
|
83
|
+
try {
|
|
84
|
+
const stats = await stat(filePath);
|
|
85
|
+
return stats.isDirectory();
|
|
86
|
+
} catch {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// src/lib/clipboard.ts
|
|
2
|
+
import { $ } from 'bun';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Copy text to clipboard (cross-platform)
|
|
6
|
+
* Returns true if successful, false if no clipboard tool available
|
|
7
|
+
*/
|
|
8
|
+
export async function copyToClipboard(text: string): Promise<boolean> {
|
|
9
|
+
// macOS
|
|
10
|
+
if (process.platform === 'darwin') {
|
|
11
|
+
const proc = Bun.spawn(['pbcopy'], { stdin: 'pipe' });
|
|
12
|
+
proc.stdin.write(text);
|
|
13
|
+
proc.stdin.end();
|
|
14
|
+
await proc.exited;
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Linux: try available clipboard tools in order of preference
|
|
19
|
+
const tools: string[][] = [
|
|
20
|
+
['xclip', '-selection', 'clipboard'],
|
|
21
|
+
['xsel', '--clipboard', '--input'],
|
|
22
|
+
['wl-copy'] // Wayland
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
for (const cmd of tools) {
|
|
26
|
+
try {
|
|
27
|
+
const which = await $`which ${cmd[0]}`.quiet();
|
|
28
|
+
if (which.exitCode === 0) {
|
|
29
|
+
const proc = Bun.spawn(cmd, { stdin: 'pipe' });
|
|
30
|
+
proc.stdin.write(text);
|
|
31
|
+
proc.stdin.end();
|
|
32
|
+
await proc.exited;
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
} catch {
|
|
36
|
+
// Tool not found, try next
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// src/lib/flags.test.ts
|
|
2
|
+
import { describe, expect, test } from 'bun:test';
|
|
3
|
+
import { parseGlobalFlags, parseUploadFlags } from './flags';
|
|
4
|
+
|
|
5
|
+
describe('parseGlobalFlags', () => {
|
|
6
|
+
test('parses --quiet flag', () => {
|
|
7
|
+
const result = parseGlobalFlags(['--quiet', 'file.txt']);
|
|
8
|
+
expect(result.quiet).toBe(true);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
test('parses -q shorthand', () => {
|
|
12
|
+
const result = parseGlobalFlags(['-q', 'file.txt']);
|
|
13
|
+
expect(result.quiet).toBe(true);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test('parses --ci flag', () => {
|
|
17
|
+
const result = parseGlobalFlags(['--ci', 'file.txt']);
|
|
18
|
+
expect(result.ci).toBe(true);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test('returns remaining args', () => {
|
|
22
|
+
const result = parseGlobalFlags(['--quiet', '--ci', 'file.txt', 'file2.txt']);
|
|
23
|
+
expect(result.args).toEqual(['file.txt', 'file2.txt']);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test('handles no flags', () => {
|
|
27
|
+
const result = parseGlobalFlags(['file.txt']);
|
|
28
|
+
expect(result.quiet).toBe(false);
|
|
29
|
+
expect(result.ci).toBe(false);
|
|
30
|
+
expect(result.args).toEqual(['file.txt']);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('passes through command-specific flags', () => {
|
|
34
|
+
const result = parseGlobalFlags(['prune', 'backups/', '--keep-last', '7', '--quiet']);
|
|
35
|
+
expect(result.quiet).toBe(true);
|
|
36
|
+
expect(result.args).toEqual(['prune', 'backups/', '--keep-last', '7']);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
describe('parseUploadFlags', () => {
|
|
41
|
+
test('parses --prefix', () => {
|
|
42
|
+
const result = parseUploadFlags(['--prefix', 'backups/daily', 'file.txt']);
|
|
43
|
+
expect(result.prefix).toBe('backups/daily');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('parses --compression', () => {
|
|
47
|
+
const result = parseUploadFlags(['--compression', '9', 'dir/']);
|
|
48
|
+
expect(result.compression).toBe(9);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('parses --as', () => {
|
|
52
|
+
const result = parseUploadFlags(['--as', 'backup.tar.gz', 'dir/']);
|
|
53
|
+
expect(result.as).toBe('backup.tar.gz');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('parses --fast and --slow', () => {
|
|
57
|
+
expect(parseUploadFlags(['--fast', 'file.txt']).fast).toBe(true);
|
|
58
|
+
expect(parseUploadFlags(['-f', 'file.txt']).fast).toBe(true);
|
|
59
|
+
expect(parseUploadFlags(['--slow', 'file.txt']).slow).toBe(true);
|
|
60
|
+
expect(parseUploadFlags(['-s', 'file.txt']).slow).toBe(true);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('returns remaining paths', () => {
|
|
64
|
+
const result = parseUploadFlags(['--prefix', 'x', 'a.txt', 'b.txt']);
|
|
65
|
+
expect(result.paths).toEqual(['a.txt', 'b.txt']);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('defaults compression to 6', () => {
|
|
69
|
+
const result = parseUploadFlags(['file.txt']);
|
|
70
|
+
expect(result.compression).toBe(6);
|
|
71
|
+
});
|
|
72
|
+
});
|
package/src/lib/flags.ts
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
// src/lib/flags.ts
|
|
2
|
+
|
|
3
|
+
export interface GlobalFlags {
|
|
4
|
+
quiet: boolean;
|
|
5
|
+
ci: boolean;
|
|
6
|
+
help: boolean;
|
|
7
|
+
version: boolean;
|
|
8
|
+
args: string[];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface UploadFlags {
|
|
12
|
+
prefix?: string;
|
|
13
|
+
compression: number;
|
|
14
|
+
as?: string;
|
|
15
|
+
fast: boolean;
|
|
16
|
+
slow: boolean;
|
|
17
|
+
paths: string[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface ListFlags {
|
|
21
|
+
json: boolean;
|
|
22
|
+
prefix?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface PruneFlags {
|
|
26
|
+
olderThan?: number;
|
|
27
|
+
keepLast?: number;
|
|
28
|
+
minAge: string;
|
|
29
|
+
dryRun: boolean;
|
|
30
|
+
prefix: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function parseGlobalFlags(args: string[]): GlobalFlags {
|
|
34
|
+
const result: GlobalFlags = {
|
|
35
|
+
args: [],
|
|
36
|
+
ci: false,
|
|
37
|
+
help: false,
|
|
38
|
+
quiet: false,
|
|
39
|
+
version: false
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
for (let i = 0; i < args.length; i++) {
|
|
43
|
+
const arg = args[i];
|
|
44
|
+
if (arg === '--quiet' || arg === '-q') {
|
|
45
|
+
result.quiet = true;
|
|
46
|
+
} else if (arg === '--ci') {
|
|
47
|
+
result.ci = true;
|
|
48
|
+
} else if (arg === '--help' || arg === '-h') {
|
|
49
|
+
result.help = true;
|
|
50
|
+
} else if (arg === '--version' || arg === '-v') {
|
|
51
|
+
result.version = true;
|
|
52
|
+
} else {
|
|
53
|
+
// Pass through everything else (including command-specific flags)
|
|
54
|
+
result.args.push(arg);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return result;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function parseUploadFlags(args: string[]): UploadFlags {
|
|
62
|
+
const result: UploadFlags = {
|
|
63
|
+
compression: 6,
|
|
64
|
+
fast: false,
|
|
65
|
+
paths: [],
|
|
66
|
+
slow: false
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
for (let i = 0; i < args.length; i++) {
|
|
70
|
+
const arg = args[i];
|
|
71
|
+
if (arg === '--prefix' && args[i + 1]) {
|
|
72
|
+
result.prefix = args[++i];
|
|
73
|
+
} else if (arg === '--compression' && args[i + 1]) {
|
|
74
|
+
result.compression = parseInt(args[++i], 10);
|
|
75
|
+
} else if (arg === '--as' && args[i + 1]) {
|
|
76
|
+
result.as = args[++i];
|
|
77
|
+
} else if (arg === '--fast' || arg === '-f') {
|
|
78
|
+
result.fast = true;
|
|
79
|
+
} else if (arg === '--slow' || arg === '-s') {
|
|
80
|
+
result.slow = true;
|
|
81
|
+
} else if (!arg.startsWith('-')) {
|
|
82
|
+
result.paths.push(arg);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return result;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function parseListFlags(args: string[]): ListFlags {
|
|
90
|
+
const result: ListFlags = {
|
|
91
|
+
json: false
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
for (let i = 0; i < args.length; i++) {
|
|
95
|
+
const arg = args[i];
|
|
96
|
+
if (arg === '--json') {
|
|
97
|
+
result.json = true;
|
|
98
|
+
} else if (!arg.startsWith('-')) {
|
|
99
|
+
result.prefix = arg;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return result;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function parsePruneFlags(args: string[]): PruneFlags {
|
|
107
|
+
const result: PruneFlags = {
|
|
108
|
+
dryRun: false,
|
|
109
|
+
minAge: '1d',
|
|
110
|
+
prefix: ''
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
for (let i = 0; i < args.length; i++) {
|
|
114
|
+
const arg = args[i];
|
|
115
|
+
if (arg === '--older-than' && args[i + 1]) {
|
|
116
|
+
result.olderThan = parseInt(args[++i], 10);
|
|
117
|
+
} else if (arg === '--keep-last' && args[i + 1]) {
|
|
118
|
+
result.keepLast = parseInt(args[++i], 10);
|
|
119
|
+
} else if (arg === '--min-age' && args[i + 1]) {
|
|
120
|
+
result.minAge = args[++i];
|
|
121
|
+
} else if (arg === '--dry-run') {
|
|
122
|
+
result.dryRun = true;
|
|
123
|
+
} else if (!arg.startsWith('-')) {
|
|
124
|
+
result.prefix = arg;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return result;
|
|
129
|
+
}
|