telstore 0.1.6 → 0.1.7
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 +68 -6
- package/bin/telstore.js +27 -7
- package/package.json +1 -1
- package/src/caption.js +40 -2
- package/src/cli.js +73 -20
- package/src/commands/delete.js +196 -1
- package/src/commands/list.js +29 -5
- package/src/commands/restore.js +118 -1
- package/src/commands/status.js +4 -4
- package/src/commands/upload.js +255 -13
- package/src/manifest.js +21 -1
- package/src/settings.js +6 -6
- package/src/sources.js +163 -0
package/src/sources.js
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { promises as fs } from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
|
|
4
|
+
// What a command line names and what telstore uploads are not the same list: a folder stands
|
|
5
|
+
// for the files inside it, and a pattern for the names that match. Both are resolved here,
|
|
6
|
+
// before anything connects, so the run is decided against the disk rather than discovered one
|
|
7
|
+
// file at a time.
|
|
8
|
+
//
|
|
9
|
+
// The shell expands `*` long before node sees it, and that is the expansion telstore prefers —
|
|
10
|
+
// this one only runs on a pattern that arrived intact: quoted, or left alone by a shell that
|
|
11
|
+
// found nothing to expand it to. Whichever did the work, the rule is the same one glob has
|
|
12
|
+
// always had, so a quoted pattern is not a different feature with different results.
|
|
13
|
+
|
|
14
|
+
// One level, because a folder is a place someone put files, not a tree telstore may walk on
|
|
15
|
+
// its own: a recursive sweep of a home directory is thousands of backups nobody asked for.
|
|
16
|
+
const HIDDEN = /^\./
|
|
17
|
+
|
|
18
|
+
function isPattern(text) {
|
|
19
|
+
return /[*?]/.test(text)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// `*` and `?` mean what they mean in a shell; everything else is a literal, including the
|
|
23
|
+
// regex metacharacters a file name is perfectly allowed to contain.
|
|
24
|
+
function patternToRegExp(pattern) {
|
|
25
|
+
const body = pattern
|
|
26
|
+
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
27
|
+
.replace(/[*?]/g, (char) => (char === '*' ? '[^/]*' : '[^/]'))
|
|
28
|
+
|
|
29
|
+
return new RegExp(`^${body}$`)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// A directory entry is only a source if it is a file — following symlinks, since a link to a
|
|
33
|
+
// file is a perfectly ordinary thing to keep in a folder of backups. Anything that cannot be
|
|
34
|
+
// read at all is named rather than dropped.
|
|
35
|
+
async function classify(full) {
|
|
36
|
+
try {
|
|
37
|
+
const stat = await fs.stat(full)
|
|
38
|
+
|
|
39
|
+
if (stat.isFile()) return { kind: 'file' }
|
|
40
|
+
if (stat.isDirectory()) return { kind: 'skip', reason: 'directory' }
|
|
41
|
+
|
|
42
|
+
return { kind: 'skip', reason: 'not a file' }
|
|
43
|
+
} catch (err) {
|
|
44
|
+
return { kind: 'skip', reason: `cannot be read (${err.code ?? err.message})` }
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function readEntries(dir) {
|
|
49
|
+
return (await fs.readdir(dir)).sort()
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function expandDirectory(dir, skipped) {
|
|
53
|
+
const paths = []
|
|
54
|
+
const folders = []
|
|
55
|
+
|
|
56
|
+
for (const name of await readEntries(dir)) {
|
|
57
|
+
if (HIDDEN.test(name)) continue
|
|
58
|
+
|
|
59
|
+
const full = path.join(dir, name)
|
|
60
|
+
const { kind, reason } = await classify(full)
|
|
61
|
+
|
|
62
|
+
if (kind === 'file') {
|
|
63
|
+
paths.push(full)
|
|
64
|
+
continue
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (reason === 'directory') folders.push(name)
|
|
68
|
+
skipped.push({ path: full, reason })
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (paths.length === 0) {
|
|
72
|
+
throw new Error(
|
|
73
|
+
`${dir} has no files to upload — telstore reads one level down and leaves hidden files ` +
|
|
74
|
+
'alone.' +
|
|
75
|
+
(folders.length > 0
|
|
76
|
+
? ` telstore does not walk into the folders inside it (${folders.join(', ')}) — ` +
|
|
77
|
+
'name one of those instead.'
|
|
78
|
+
: ''),
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return paths
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function expandPattern(pattern) {
|
|
86
|
+
const dir = path.dirname(pattern)
|
|
87
|
+
const base = path.basename(pattern)
|
|
88
|
+
|
|
89
|
+
if (isPattern(dir)) {
|
|
90
|
+
throw new Error(
|
|
91
|
+
`Wildcards only work in the last part of a path, and ${pattern} has one earlier: ` +
|
|
92
|
+
'telstore does not walk folders looking for matches.',
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
let entries
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
entries = await readEntries(dir)
|
|
100
|
+
} catch (err) {
|
|
101
|
+
if (err.code === 'ENOENT') {
|
|
102
|
+
throw new Error(`No file matches ${pattern}: ${dir} does not exist.`)
|
|
103
|
+
}
|
|
104
|
+
throw err
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const matches = patternToRegExp(base)
|
|
108
|
+
const paths = []
|
|
109
|
+
|
|
110
|
+
for (const name of entries) {
|
|
111
|
+
// A pattern that does not start with a dot does not go looking for hidden files, the same
|
|
112
|
+
// rule every shell uses — otherwise `telstore '*'` in a home directory sends .ssh upwards.
|
|
113
|
+
if (HIDDEN.test(name) && !HIDDEN.test(base)) continue
|
|
114
|
+
if (!matches.test(name)) continue
|
|
115
|
+
|
|
116
|
+
const full = path.join(dir, name)
|
|
117
|
+
if ((await classify(full)).kind === 'file') paths.push(full)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (paths.length === 0) {
|
|
121
|
+
throw new Error(
|
|
122
|
+
`No file matches ${pattern} in ${dir}. Note that the shell usually expands patterns ` +
|
|
123
|
+
'itself, so a quoted one reaches telstore exactly as typed.',
|
|
124
|
+
)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return paths
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Turns the positionals of an upload into the list of files it will actually send.
|
|
132
|
+
* Returns `{ paths, skipped }`; a name that is neither a folder nor a pattern is handed on
|
|
133
|
+
* untouched, missing ones included, so "File does not exist" keeps coming from one place.
|
|
134
|
+
*/
|
|
135
|
+
export async function expandSources(args) {
|
|
136
|
+
const paths = []
|
|
137
|
+
const skipped = []
|
|
138
|
+
|
|
139
|
+
for (const arg of args) {
|
|
140
|
+
if (isPattern(arg)) {
|
|
141
|
+
paths.push(...(await expandPattern(arg)))
|
|
142
|
+
continue
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
let stat = null
|
|
146
|
+
|
|
147
|
+
try {
|
|
148
|
+
stat = await fs.stat(arg)
|
|
149
|
+
} catch {
|
|
150
|
+
paths.push(arg)
|
|
151
|
+
continue
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (stat.isDirectory()) {
|
|
155
|
+
paths.push(...(await expandDirectory(arg, skipped)))
|
|
156
|
+
continue
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
paths.push(arg)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return { paths, skipped }
|
|
163
|
+
}
|