Skip to content

Commit 671b16d

Browse files
committed
support ignore glob patterns during ingest
1 parent 0f68319 commit 671b16d

File tree

4 files changed

+193
-12
lines changed

4 files changed

+193
-12
lines changed

README.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ npx staticvault <command> [arguments]
5252
Change vault password.
5353

5454
```bash
55-
chpass <vault> [-p password] [-n newpassword]
55+
npx staticvault chpass <vault> [-p password] [-n newpassword]
5656
```
5757

5858
## `dump`
@@ -73,10 +73,11 @@ npx staticvault init <vault> [-p password] [-d difficulty]
7373

7474
## `ingest`
7575

76-
Encrypt and add files to an existing vault.
76+
Encrypt and add files to an existing vault. Use `-i` to ignore files/folders based on a pattern
77+
(`*`, `**`, and `?` supported).
7778

7879
```bash
79-
npx staticvault ingest <vault> <source> [-p password]
80+
npx staticvault ingest <vault> <source> [-i ignore]+ [-p password]
8081
```
8182

8283
## `rekey`

dist/cli.js

Lines changed: 79 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -935,12 +935,13 @@ Current command:`);
935935
}
936936
if (!filter || filter === "ingest") {
937937
console.log(`
938-
- ingest <vault> <source> [-p password]
938+
- ingest <vault> <source> [-i ignore]+ [-p password]
939939
940940
Encrypt and copy source folders/files into vault.
941941
942942
<vault> Vault directory
943943
<source> Source directory
944+
[-i ignore]+ Ignore files/folders that pattern (*, **, ? supported)
944945
[-p password] Encryption password`);
945946
}
946947
if (!filter || filter === "init") {
@@ -1041,6 +1042,27 @@ function treeChars(depth, last, folder, status) {
10411042
}
10421043
return { prefix, postfix };
10431044
}
1045+
function glob(pattern, path2, name, dir) {
1046+
const neg = pattern.startsWith("!");
1047+
if (neg || pattern.startsWith("\\!")) {
1048+
pattern = pattern.substr(1);
1049+
}
1050+
const firstSep = pattern.indexOf("/");
1051+
if (firstSep >= 0 && firstSep < pattern.length - 1) {
1052+
name = path2 + "/" + name;
1053+
}
1054+
if (pattern.endsWith("/")) {
1055+
if (!dir) {
1056+
return false;
1057+
}
1058+
pattern = pattern.substr(0, pattern.length - 1);
1059+
}
1060+
const regex = new RegExp(
1061+
"^" + pattern.split("**").map((p) => p.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, "[^/]*").replace(/\?/g, "[^/]")).join(".*") + "$"
1062+
);
1063+
const result = regex.test(name);
1064+
return neg ? !result : result;
1065+
}
10441066
async function cmdChpass(args) {
10451067
let target = null;
10461068
let password = null;
@@ -1196,6 +1218,7 @@ async function cmdIngest(args) {
11961218
let target = null;
11971219
let source = null;
11981220
let password = null;
1221+
const ignore = [];
11991222
for (; ; ) {
12001223
const arg = args.shift();
12011224
if (typeof arg === "undefined") break;
@@ -1215,6 +1238,15 @@ Missing password`);
12151238
Cannot specify password more than once`);
12161239
return 1;
12171240
}
1241+
} else if (arg === "-i") {
1242+
const ig = args.shift();
1243+
if (typeof ig === "undefined") {
1244+
printUsage("ingest");
1245+
console.error(`
1246+
Missing ignore pattern`);
1247+
return 1;
1248+
}
1249+
ignore.push(ig);
12181250
} else if (target === null) {
12191251
target = arg;
12201252
} else if (source === null) {
@@ -1271,15 +1303,24 @@ Missing source directory`);
12711303
success: "(copied!)",
12721304
linked: "(linked!)"
12731305
};
1306+
const shouldIgnore = (name, dir) => ignore.some((p) => glob(p, vault.getPath().substr(1), name, dir));
12741307
for (let i = 0; i < items.length; i++) {
12751308
const { name, dir } = items[i];
12761309
const full = path.join(src, name);
12771310
let status = "?";
12781311
if (dir) {
1279-
status = statusMap[await vault.putFolder(name)];
1312+
if (shouldIgnore(name, true)) {
1313+
status = "(ignored)";
1314+
} else {
1315+
status = statusMap[await vault.putFolder(name)];
1316+
}
12801317
} else {
1281-
const bytes = await srcIO.read(full);
1282-
status = statusMap[await vault.putFile(name, bytes)];
1318+
if (shouldIgnore(name, false)) {
1319+
status = "(ignored)";
1320+
} else {
1321+
const bytes = await srcIO.read(full);
1322+
status = statusMap[await vault.putFile(name, bytes)];
1323+
}
12831324
}
12841325
const { prefix, postfix } = treeChars(depth, i >= items.length - 1, dir, status);
12851326
console.log(prefix + name + postfix);
@@ -1658,6 +1699,40 @@ async function cmdTest(args) {
16581699
throw new Error(`Failed to decrypt hello2.txt`);
16591700
}
16601701
}
1702+
{
1703+
const testGlob = (result, pat, path2, name, dir) => {
1704+
const r = glob(pat, path2, name, dir);
1705+
if (r !== result) {
1706+
throw new Error(`Expecting ${result}: glob("${pat}", "${path2}", "${name}", ${dir})`);
1707+
}
1708+
};
1709+
testGlob(true, ".DS_Store", "foo/bar", ".DS_Store", false);
1710+
testGlob(false, ".DS_Store", "foo/bar", "not_DS_Store", false);
1711+
testGlob(true, "foo/bar.txt", "foo", "bar.txt", false);
1712+
testGlob(true, "foo/bar.txt", "foo", "bar.txt", true);
1713+
testGlob(false, "foo/bar.txt", "foo", "baz.txt", false);
1714+
testGlob(false, "foo/bar.txt", "other", "bar.txt", false);
1715+
testGlob(true, "build/", "", "build", true);
1716+
testGlob(false, "build/", "", "build", false);
1717+
testGlob(true, "foo/bar/", "foo", "bar", true);
1718+
testGlob(false, "foo/bar/", "foo", "bar", false);
1719+
testGlob(true, "*.log", "logs", "error.log", false);
1720+
testGlob(false, "*.log", "logs", "error.txt", false);
1721+
testGlob(true, "**/temp", "foo/bar", "temp", true);
1722+
testGlob(true, "**/temp", "", "temp", true);
1723+
testGlob(false, "**/temp", "foo/bar", "not_temp", true);
1724+
testGlob(true, "**/*.bak", "foo/bar", "data.bak", false);
1725+
testGlob(false, "**/*.bak", "foo/bar", "data.txt", false);
1726+
testGlob(true, "file?.txt", "", "file1.txt", false);
1727+
testGlob(false, "file?.txt", "", "file10.txt", false);
1728+
testGlob(false, "!secret.txt", "", "secret.txt", false);
1729+
testGlob(true, "!secret.txt", "", "visible.txt", false);
1730+
testGlob(true, "\\!secret.txt", "", "!secret.txt", false);
1731+
testGlob(true, "/root.txt", "", "root.txt", false);
1732+
testGlob(false, "/root.txt", "subdir", "root.txt", false);
1733+
testGlob(true, "a/**/z.js", "a/b/c", "z.js", false);
1734+
testGlob(false, "a/**/z.js", "x/y/z", "z.js", false);
1735+
}
16611736
console.log("success!");
16621737
return 0;
16631738
}

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "staticvault",
3-
"version": "1.1.2",
3+
"version": "1.1.3",
44
"description": "Encrypt, host, and share files on a static website",
55
"author": "velipso",
66
"license": "0BSD",

src/cli.ts

Lines changed: 109 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,12 +47,13 @@ function printUsage(filter?: string) {
4747
}
4848
if (!filter || filter === 'ingest') {
4949
console.log(`
50-
- ingest <vault> <source> [-p password]
50+
- ingest <vault> <source> [-i ignore]+ [-p password]
5151
5252
Encrypt and copy source folders/files into vault.
5353
5454
<vault> Vault directory
5555
<source> Source directory
56+
[-i ignore]+ Ignore files/folders that pattern (*, **, ? supported)
5657
[-p password] Encryption password`);
5758
}
5859
if (!filter || filter === 'init') {
@@ -158,6 +159,37 @@ function treeChars(depth: number, last: boolean, folder: boolean, status?: strin
158159
return { prefix, postfix };
159160
}
160161

162+
function glob(pattern: string, path: string, name: string, dir: boolean): boolean {
163+
const neg = pattern.startsWith('!');
164+
if (neg || pattern.startsWith('\\!')) {
165+
pattern = pattern.substr(1);
166+
}
167+
const firstSep = pattern.indexOf('/');
168+
if (firstSep >= 0 && firstSep < pattern.length - 1) {
169+
// matching full path
170+
name = path + '/' + name;
171+
}
172+
if (pattern.endsWith('/')) {
173+
if (!dir) {
174+
return false;
175+
}
176+
pattern = pattern.substr(0, pattern.length - 1);
177+
}
178+
const regex = new RegExp(
179+
'^' +
180+
pattern
181+
.split('**')
182+
.map(p => p
183+
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
184+
.replace(/\*/g, '[^/]*')
185+
.replace(/\?/g, '[^/]'))
186+
.join('.*') +
187+
'$'
188+
);
189+
const result = regex.test(name);
190+
return neg ? !result : result;
191+
}
192+
161193
async function cmdChpass(args: string[]): Promise<number> {
162194
let target: string | null = null;
163195
let password: string | null = null;
@@ -304,6 +336,7 @@ async function cmdIngest(args: string[]): Promise<number> {
304336
let target: string | null = null;
305337
let source: string | null = null;
306338
let password: string | null = null;
339+
const ignore: string[] = [];
307340
for (;;) {
308341
const arg = args.shift();
309342
if (typeof arg === 'undefined') break;
@@ -321,6 +354,14 @@ async function cmdIngest(args: string[]): Promise<number> {
321354
console.error(`\nCannot specify password more than once`);
322355
return 1;
323356
}
357+
} else if (arg === '-i') {
358+
const ig = args.shift();
359+
if (typeof ig === 'undefined') {
360+
printUsage('ingest');
361+
console.error(`\nMissing ignore pattern`);
362+
return 1;
363+
}
364+
ignore.push(ig);
324365
} else if (target === null) {
325366
target = arg;
326367
} else if (source === null) {
@@ -374,15 +415,25 @@ async function cmdIngest(args: string[]): Promise<number> {
374415
success: '(copied!)',
375416
linked: '(linked!)'
376417
};
418+
const shouldIgnore = (name: string, dir: boolean) =>
419+
ignore.some(p => glob(p, vault.getPath().substr(1), name, dir));
377420
for (let i = 0; i < items.length; i++) {
378421
const { name, dir } = items[i];
379422
const full = path.join(src, name);
380423
let status = '?';
381424
if (dir) {
382-
status = statusMap[await vault.putFolder(name)];
425+
if (shouldIgnore(name, true)) {
426+
status = '(ignored)';
427+
} else {
428+
status = statusMap[await vault.putFolder(name)];
429+
}
383430
} else {
384-
const bytes = await srcIO.read(full);
385-
status = statusMap[await vault.putFile(name, bytes)];
431+
if (shouldIgnore(name, false)) {
432+
status = '(ignored)';
433+
} else {
434+
const bytes = await srcIO.read(full);
435+
status = statusMap[await vault.putFile(name, bytes)];
436+
}
386437
}
387438
const { prefix, postfix } = treeChars(depth, i >= items.length - 1, dir, status);
388439
console.log(prefix + name + postfix);
@@ -770,6 +821,60 @@ async function cmdTest(args: string[]): Promise<number> {
770821
}
771822
}
772823

824+
{
825+
const testGlob = (result: boolean, pat: string, path: string, name: string, dir: boolean) => {
826+
const r = glob(pat, path, name, dir);
827+
if (r !== result) {
828+
throw new Error(`Expecting ${result}: glob("${pat}", "${path}", "${name}", ${dir})`);
829+
}
830+
}
831+
// Match by name anywhere
832+
testGlob(true, '.DS_Store', 'foo/bar', '.DS_Store', false);
833+
testGlob(false, '.DS_Store', 'foo/bar', 'not_DS_Store', false);
834+
835+
// Match full path
836+
testGlob(true, 'foo/bar.txt', 'foo', 'bar.txt', false);
837+
testGlob(true, 'foo/bar.txt', 'foo', 'bar.txt', true);
838+
testGlob(false, 'foo/bar.txt', 'foo', 'baz.txt', false);
839+
testGlob(false, 'foo/bar.txt', 'other', 'bar.txt', false);
840+
841+
// Match directories with trailing slash
842+
testGlob(true, 'build/', '', 'build', true);
843+
testGlob(false, 'build/', '', 'build', false);
844+
testGlob(true, 'foo/bar/', 'foo', 'bar', true);
845+
testGlob(false, 'foo/bar/', 'foo', 'bar', false);
846+
847+
// Match wildcard in name
848+
testGlob(true, '*.log', 'logs', 'error.log', false);
849+
testGlob(false, '*.log', 'logs', 'error.txt', false);
850+
851+
// Match wildcard in path
852+
testGlob(true, '**/temp', 'foo/bar', 'temp', true);
853+
testGlob(true, '**/temp', '', 'temp', true);
854+
testGlob(false, '**/temp', 'foo/bar', 'not_temp', true);
855+
856+
// Match file deeply
857+
testGlob(true, '**/*.bak', 'foo/bar', 'data.bak', false);
858+
testGlob(false, '**/*.bak', 'foo/bar', 'data.txt', false);
859+
860+
// Single-char wildcard
861+
testGlob(true, 'file?.txt', '', 'file1.txt', false);
862+
testGlob(false, 'file?.txt', '', 'file10.txt', false);
863+
864+
// Negated pattern
865+
testGlob(false, '!secret.txt', '', 'secret.txt', false);
866+
testGlob(true, '!secret.txt', '', 'visible.txt', false);
867+
testGlob(true, '\\!secret.txt', '', '!secret.txt', false);
868+
869+
// Leading slash (matches from root)
870+
testGlob(true, '/root.txt', '', 'root.txt', false);
871+
testGlob(false, '/root.txt', 'subdir', 'root.txt', false);
872+
873+
// Match nested folder/file
874+
testGlob(true, 'a/**/z.js', 'a/b/c', 'z.js', false);
875+
testGlob(false, 'a/**/z.js', 'x/y/z', 'z.js', false);
876+
}
877+
773878
console.log('success!');
774879
return 0;
775880
}

0 commit comments

Comments
 (0)