mirror of
https://github.com/andrewboldi/obsidian-vault-sync.git
synced 2026-07-22 06:53:35 +00:00
Phase 1: tarball-based seed via streaming fetch
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
commit
61d5c0edeb
13 changed files with 1273 additions and 0 deletions
12
.gitignore
vendored
Normal file
12
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
node_modules/
|
||||
.DS_Store
|
||||
*.log
|
||||
.env
|
||||
data.json
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
|
||||
# Build artifacts (main.js IS committed for BRAT)
|
||||
*.map
|
||||
32
esbuild.config.mjs
Normal file
32
esbuild.config.mjs
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import esbuild from "esbuild";
|
||||
import process from "process";
|
||||
|
||||
const banner = `/*
|
||||
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
|
||||
if you want to view the source visit https://github.com/andrewboldi/obsidian-vault-sync
|
||||
*/
|
||||
`;
|
||||
|
||||
const prod = process.argv[2] === "production";
|
||||
|
||||
const context = await esbuild.context({
|
||||
banner: { js: banner },
|
||||
entryPoints: ["src/main.ts"],
|
||||
bundle: true,
|
||||
external: ["obsidian", "electron"],
|
||||
format: "cjs",
|
||||
target: "es2020",
|
||||
logLevel: "info",
|
||||
sourcemap: prod ? false : "inline",
|
||||
treeShaking: true,
|
||||
platform: "browser",
|
||||
minify: prod,
|
||||
outfile: "main.js",
|
||||
});
|
||||
|
||||
if (prod) {
|
||||
await context.rebuild();
|
||||
process.exit(0);
|
||||
} else {
|
||||
await context.watch();
|
||||
}
|
||||
6
main.js
Normal file
6
main.js
Normal file
File diff suppressed because one or more lines are too long
10
manifest.json
Normal file
10
manifest.json
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"id": "obsidian-vault-sync",
|
||||
"name": "Vault Sync (REST)",
|
||||
"version": "0.1.0",
|
||||
"minAppVersion": "1.4.0",
|
||||
"description": "Sync Obsidian vault with GitHub via REST API. Works on iOS for any vault size including images.",
|
||||
"author": "Andrew Boldi",
|
||||
"authorUrl": "https://github.com/andrewboldi/obsidian-vault-sync",
|
||||
"isDesktopOnly": false
|
||||
}
|
||||
633
package-lock.json
generated
Normal file
633
package-lock.json
generated
Normal file
|
|
@ -0,0 +1,633 @@
|
|||
{
|
||||
"name": "obsidian-vault-sync",
|
||||
"version": "0.1.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "obsidian-vault-sync",
|
||||
"version": "0.1.0",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.11.0",
|
||||
"esbuild": "^0.24.2",
|
||||
"obsidian": "^1.4.0",
|
||||
"tslib": "^2.6.2",
|
||||
"typescript": "^5.3.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/state": {
|
||||
"version": "6.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.0.tgz",
|
||||
"integrity": "sha512-MwBHVK60IiIHDcoMet78lxt6iw5gJOGSbNbOIVBHWVXIH4/Nq1+GQgLLGgI1KlnN86WDXsPudVaqYHKBIx7Eyw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@marijn/find-cluster-break": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/view": {
|
||||
"version": "6.38.6",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.38.6.tgz",
|
||||
"integrity": "sha512-qiS0z1bKs5WOvHIAC0Cybmv4AJSkAXgX5aD6Mqd2epSLlVJsQl8NG23jCVouIgkh4All/mrbdsf2UOLFnJw0tw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@codemirror/state": "^6.5.0",
|
||||
"crelt": "^1.0.6",
|
||||
"style-mod": "^4.1.0",
|
||||
"w3c-keyname": "^2.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.24.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.2.tgz",
|
||||
"integrity": "sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"aix"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm": {
|
||||
"version": "0.24.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.24.2.tgz",
|
||||
"integrity": "sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm64": {
|
||||
"version": "0.24.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.24.2.tgz",
|
||||
"integrity": "sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-x64": {
|
||||
"version": "0.24.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.24.2.tgz",
|
||||
"integrity": "sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-arm64": {
|
||||
"version": "0.24.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.24.2.tgz",
|
||||
"integrity": "sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-x64": {
|
||||
"version": "0.24.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.24.2.tgz",
|
||||
"integrity": "sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-arm64": {
|
||||
"version": "0.24.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.2.tgz",
|
||||
"integrity": "sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-x64": {
|
||||
"version": "0.24.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.24.2.tgz",
|
||||
"integrity": "sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm": {
|
||||
"version": "0.24.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.24.2.tgz",
|
||||
"integrity": "sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm64": {
|
||||
"version": "0.24.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.24.2.tgz",
|
||||
"integrity": "sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ia32": {
|
||||
"version": "0.24.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.24.2.tgz",
|
||||
"integrity": "sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-loong64": {
|
||||
"version": "0.24.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.24.2.tgz",
|
||||
"integrity": "sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-mips64el": {
|
||||
"version": "0.24.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.24.2.tgz",
|
||||
"integrity": "sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ppc64": {
|
||||
"version": "0.24.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.24.2.tgz",
|
||||
"integrity": "sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-riscv64": {
|
||||
"version": "0.24.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.24.2.tgz",
|
||||
"integrity": "sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-s390x": {
|
||||
"version": "0.24.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.24.2.tgz",
|
||||
"integrity": "sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-x64": {
|
||||
"version": "0.24.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.24.2.tgz",
|
||||
"integrity": "sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-arm64": {
|
||||
"version": "0.24.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.24.2.tgz",
|
||||
"integrity": "sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-x64": {
|
||||
"version": "0.24.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.24.2.tgz",
|
||||
"integrity": "sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-arm64": {
|
||||
"version": "0.24.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.2.tgz",
|
||||
"integrity": "sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.24.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.24.2.tgz",
|
||||
"integrity": "sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/sunos-x64": {
|
||||
"version": "0.24.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.2.tgz",
|
||||
"integrity": "sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"sunos"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-arm64": {
|
||||
"version": "0.24.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.24.2.tgz",
|
||||
"integrity": "sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-ia32": {
|
||||
"version": "0.24.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.24.2.tgz",
|
||||
"integrity": "sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-x64": {
|
||||
"version": "0.24.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.24.2.tgz",
|
||||
"integrity": "sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@marijn/find-cluster-break": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz",
|
||||
"integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@types/codemirror": {
|
||||
"version": "5.60.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-5.60.8.tgz",
|
||||
"integrity": "sha512-VjFgDF/eB+Aklcy15TtOTLQeMjTo07k7KAjql8OK5Dirr7a6sJY4T1uVBDuTVG9VEmn1uUsohOpYnVfgC6/jyw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/tern": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/estree": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
||||
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "20.19.39",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.39.tgz",
|
||||
"integrity": "sha512-orrrD74MBUyK8jOAD/r0+lfa1I2MO6I+vAkmAWzMYbCcgrN4lCrmK52gRFQq/JRxfYPfonkr4b0jcY7Olqdqbw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/tern": {
|
||||
"version": "0.23.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/tern/-/tern-0.23.9.tgz",
|
||||
"integrity": "sha512-ypzHFE/wBzh+BlH6rrBgS5I/Z7RD21pGhZ2rltb/+ZrVM1awdZwjx7hE5XfuYgHWk9uvV5HLZN3SloevCAp3Bw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/crelt": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz",
|
||||
"integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.24.2",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.2.tgz",
|
||||
"integrity": "sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"esbuild": "bin/esbuild"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/aix-ppc64": "0.24.2",
|
||||
"@esbuild/android-arm": "0.24.2",
|
||||
"@esbuild/android-arm64": "0.24.2",
|
||||
"@esbuild/android-x64": "0.24.2",
|
||||
"@esbuild/darwin-arm64": "0.24.2",
|
||||
"@esbuild/darwin-x64": "0.24.2",
|
||||
"@esbuild/freebsd-arm64": "0.24.2",
|
||||
"@esbuild/freebsd-x64": "0.24.2",
|
||||
"@esbuild/linux-arm": "0.24.2",
|
||||
"@esbuild/linux-arm64": "0.24.2",
|
||||
"@esbuild/linux-ia32": "0.24.2",
|
||||
"@esbuild/linux-loong64": "0.24.2",
|
||||
"@esbuild/linux-mips64el": "0.24.2",
|
||||
"@esbuild/linux-ppc64": "0.24.2",
|
||||
"@esbuild/linux-riscv64": "0.24.2",
|
||||
"@esbuild/linux-s390x": "0.24.2",
|
||||
"@esbuild/linux-x64": "0.24.2",
|
||||
"@esbuild/netbsd-arm64": "0.24.2",
|
||||
"@esbuild/netbsd-x64": "0.24.2",
|
||||
"@esbuild/openbsd-arm64": "0.24.2",
|
||||
"@esbuild/openbsd-x64": "0.24.2",
|
||||
"@esbuild/sunos-x64": "0.24.2",
|
||||
"@esbuild/win32-arm64": "0.24.2",
|
||||
"@esbuild/win32-ia32": "0.24.2",
|
||||
"@esbuild/win32-x64": "0.24.2"
|
||||
}
|
||||
},
|
||||
"node_modules/moment": {
|
||||
"version": "2.29.4",
|
||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz",
|
||||
"integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/obsidian": {
|
||||
"version": "1.12.3",
|
||||
"resolved": "https://registry.npmjs.org/obsidian/-/obsidian-1.12.3.tgz",
|
||||
"integrity": "sha512-HxWqe763dOqzXjnNiHmAJTRERN8KILBSqxDSEqbeSr7W8R8Jxezzbca+nz1LiiqXnMpM8lV2jzAezw3CZ4xNUw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/codemirror": "5.60.8",
|
||||
"moment": "2.29.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@codemirror/state": "6.5.0",
|
||||
"@codemirror/view": "6.38.6"
|
||||
}
|
||||
},
|
||||
"node_modules/style-mod": {
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.3.tgz",
|
||||
"integrity": "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"dev": true,
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.9.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/w3c-keyname": {
|
||||
"version": "2.2.8",
|
||||
"resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
|
||||
"integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
}
|
||||
}
|
||||
}
|
||||
26
package.json
Normal file
26
package.json
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"name": "obsidian-vault-sync",
|
||||
"version": "0.1.0",
|
||||
"description": "Sync Obsidian vault with GitHub via REST API. Works on iOS for any vault size including images.",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
"dev": "node esbuild.config.mjs",
|
||||
"build": "node esbuild.config.mjs production"
|
||||
},
|
||||
"keywords": [
|
||||
"obsidian",
|
||||
"obsidian-plugin",
|
||||
"github",
|
||||
"sync",
|
||||
"ios"
|
||||
],
|
||||
"author": "Andrew Boldi",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.11.0",
|
||||
"esbuild": "^0.24.2",
|
||||
"obsidian": "^1.4.0",
|
||||
"tslib": "^2.6.2",
|
||||
"typescript": "^5.3.3"
|
||||
}
|
||||
}
|
||||
54
src/github.ts
Normal file
54
src/github.ts
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
// GitHub REST helpers for Vault Sync.
|
||||
// Phase 1 only needs: parse repo URL, fetch latest branch SHA, build tarball URL.
|
||||
|
||||
export interface RepoRef {
|
||||
owner: string;
|
||||
repo: string;
|
||||
}
|
||||
|
||||
export const USER_AGENT = "obsidian-vault-sync";
|
||||
export const API_BASE = "https://api.github.com";
|
||||
|
||||
export function authHeaders(pat: string): Record<string, string> {
|
||||
return {
|
||||
Authorization: `token ${pat}`,
|
||||
"User-Agent": USER_AGENT,
|
||||
Accept: "application/vnd.github+json",
|
||||
"X-GitHub-Api-Version": "2022-11-28",
|
||||
};
|
||||
}
|
||||
|
||||
export function parseRepoUrl(url: string): RepoRef {
|
||||
if (!url) throw new Error("Repository URL is empty");
|
||||
let cleaned = url.trim().replace(/\.git$/, "").replace(/\/+$/, "");
|
||||
// Allow either full URL or "owner/repo".
|
||||
let m = cleaned.match(/^https?:\/\/github\.com\/([^/]+)\/([^/]+)$/i);
|
||||
if (!m) m = cleaned.match(/^([^/\s]+)\/([^/\s]+)$/);
|
||||
if (!m) throw new Error(`Cannot parse repo URL: ${url}`);
|
||||
return { owner: m[1], repo: m[2] };
|
||||
}
|
||||
|
||||
export async function getBranchSha(
|
||||
ref: RepoRef,
|
||||
branch: string,
|
||||
pat: string
|
||||
): Promise<string> {
|
||||
const url = `${API_BASE}/repos/${ref.owner}/${ref.repo}/branches/${encodeURIComponent(
|
||||
branch
|
||||
)}`;
|
||||
const res = await fetch(url, { headers: authHeaders(pat) });
|
||||
if (!res.ok) {
|
||||
const body = await res.text();
|
||||
throw new Error(
|
||||
`GitHub branch lookup failed: ${res.status} ${res.statusText} — ${body.slice(0, 300)}`
|
||||
);
|
||||
}
|
||||
const data = (await res.json()) as { commit?: { sha?: string } };
|
||||
const sha = data?.commit?.sha;
|
||||
if (!sha) throw new Error("GitHub response missing commit.sha");
|
||||
return sha;
|
||||
}
|
||||
|
||||
export function tarballUrl(ref: RepoRef, sha: string): string {
|
||||
return `${API_BASE}/repos/${ref.owner}/${ref.repo}/tarball/${sha}`;
|
||||
}
|
||||
39
src/main.ts
Normal file
39
src/main.ts
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
import { Notice, Plugin } from "obsidian";
|
||||
import {
|
||||
DEFAULT_SETTINGS,
|
||||
VaultSyncSettingTab,
|
||||
type VaultSyncSettings,
|
||||
} from "./settings";
|
||||
import { seedFromGitHub } from "./seed";
|
||||
|
||||
export default class VaultSyncPlugin extends Plugin {
|
||||
settings!: VaultSyncSettings;
|
||||
|
||||
async onload(): Promise<void> {
|
||||
await this.loadSettings();
|
||||
this.addSettingTab(new VaultSyncSettingTab(this.app, this));
|
||||
|
||||
this.addCommand({
|
||||
id: "seed-from-github",
|
||||
name: "Seed from GitHub",
|
||||
callback: async () => {
|
||||
try {
|
||||
await seedFromGitHub(this);
|
||||
} catch (e) {
|
||||
const msg = e instanceof Error ? e.message : String(e);
|
||||
console.error("[Vault Sync] seed failed", e);
|
||||
new Notice(`Vault Sync seed failed: ${msg}`, 10000);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async loadSettings(): Promise<void> {
|
||||
const data = (await this.loadData()) ?? {};
|
||||
this.settings = Object.assign({}, DEFAULT_SETTINGS, data);
|
||||
}
|
||||
|
||||
async saveSettings(): Promise<void> {
|
||||
await this.saveData(this.settings);
|
||||
}
|
||||
}
|
||||
141
src/seed.ts
Normal file
141
src/seed.ts
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
import { Notice } from "obsidian";
|
||||
import type VaultSyncPlugin from "./main";
|
||||
import {
|
||||
authHeaders,
|
||||
getBranchSha,
|
||||
parseRepoUrl,
|
||||
tarballUrl,
|
||||
} from "./github";
|
||||
import { parseTar } from "./tar";
|
||||
|
||||
function formatBytes(n: number): string {
|
||||
if (n < 1024) return `${n} B`;
|
||||
if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
|
||||
if (n < 1024 * 1024 * 1024) return `${(n / (1024 * 1024)).toFixed(1)} MB`;
|
||||
return `${(n / (1024 * 1024 * 1024)).toFixed(2)} GB`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Strip GitHub's wrapper directory ("{owner}-{repo}-{shortsha}/") from a tar entry path.
|
||||
* Returns null if the result would be empty (i.e. the wrapper dir itself).
|
||||
*/
|
||||
function stripWrapperDir(path: string): string | null {
|
||||
const normalized = path.replace(/\\/g, "/");
|
||||
const idx = normalized.indexOf("/");
|
||||
if (idx < 0) return null; // wrapper dir entry alone
|
||||
const rest = normalized.slice(idx + 1);
|
||||
if (!rest || rest === "/") return null;
|
||||
// Defense in depth: refuse path traversal.
|
||||
if (rest.startsWith("/") || rest.includes("../")) return null;
|
||||
return rest;
|
||||
}
|
||||
|
||||
async function ensureDir(plugin: VaultSyncPlugin, dirPath: string): Promise<void> {
|
||||
if (!dirPath || dirPath === "/" || dirPath === ".") return;
|
||||
try {
|
||||
await plugin.app.vault.adapter.mkdir(dirPath);
|
||||
} catch (_e) {
|
||||
// Already exists, or parent created concurrently — ignore.
|
||||
}
|
||||
}
|
||||
|
||||
async function ensureParentDirs(
|
||||
plugin: VaultSyncPlugin,
|
||||
filePath: string
|
||||
): Promise<void> {
|
||||
const parts = filePath.split("/");
|
||||
parts.pop(); // remove file name
|
||||
if (parts.length === 0) return;
|
||||
let acc = "";
|
||||
for (const p of parts) {
|
||||
if (!p) continue;
|
||||
acc = acc ? `${acc}/${p}` : p;
|
||||
await ensureDir(plugin, acc);
|
||||
}
|
||||
}
|
||||
|
||||
export async function seedFromGitHub(plugin: VaultSyncPlugin): Promise<void> {
|
||||
const { pat, repoUrl, branch } = plugin.settings;
|
||||
if (!pat) throw new Error("Set a GitHub PAT in Vault Sync settings.");
|
||||
if (!repoUrl) throw new Error("Set a repo URL in Vault Sync settings.");
|
||||
|
||||
const ref = parseRepoUrl(repoUrl);
|
||||
const notice = new Notice("Vault Sync: resolving branch…", 0);
|
||||
|
||||
let sha: string;
|
||||
try {
|
||||
sha = await getBranchSha(ref, branch || "main", pat);
|
||||
} catch (e) {
|
||||
notice.hide();
|
||||
throw e;
|
||||
}
|
||||
|
||||
notice.setMessage(`Vault Sync: downloading ${ref.owner}/${ref.repo}@${sha.slice(0, 7)}…`);
|
||||
|
||||
const url = tarballUrl(ref, sha);
|
||||
const res = await fetch(url, { headers: authHeaders(pat) });
|
||||
if (!res.ok || !res.body) {
|
||||
notice.hide();
|
||||
const body = res.body ? await res.text().catch(() => "") : "";
|
||||
throw new Error(
|
||||
`Tarball fetch failed: ${res.status} ${res.statusText} — ${body.slice(0, 300)}`
|
||||
);
|
||||
}
|
||||
|
||||
// GitHub serves tarballs with Content-Encoding: gzip already applied to a
|
||||
// *tar.gz* payload. fetch() automatically decompresses transport-level
|
||||
// Content-Encoding, but the body itself is still a gzipped tar — we must
|
||||
// pipe through DecompressionStream('gzip') ourselves.
|
||||
let tarStream: ReadableStream<Uint8Array>;
|
||||
try {
|
||||
tarStream = res.body.pipeThrough(new DecompressionStream("gzip"));
|
||||
} catch (e) {
|
||||
notice.hide();
|
||||
throw new Error(
|
||||
"DecompressionStream('gzip') is not available in this WebView. " +
|
||||
"Vault Sync requires iOS 16.4+/modern Chromium. " +
|
||||
String(e)
|
||||
);
|
||||
}
|
||||
|
||||
let fileCount = 0;
|
||||
let byteCount = 0;
|
||||
let lastUiUpdate = 0;
|
||||
|
||||
for await (const entry of parseTar(tarStream)) {
|
||||
const stripped = stripWrapperDir(entry.name);
|
||||
if (!stripped) continue;
|
||||
|
||||
if (entry.type === "dir") {
|
||||
await ensureDir(plugin, stripped.replace(/\/$/, ""));
|
||||
continue;
|
||||
}
|
||||
|
||||
// File.
|
||||
await ensureParentDirs(plugin, stripped);
|
||||
// Convert Uint8Array view -> standalone ArrayBuffer for writeBinary.
|
||||
const ab = entry.content.buffer.slice(
|
||||
entry.content.byteOffset,
|
||||
entry.content.byteOffset + entry.content.byteLength
|
||||
) as ArrayBuffer;
|
||||
await plugin.app.vault.adapter.writeBinary(stripped, ab);
|
||||
|
||||
fileCount++;
|
||||
byteCount += entry.content.byteLength;
|
||||
|
||||
if (fileCount - lastUiUpdate >= 25) {
|
||||
notice.setMessage(
|
||||
`Vault Sync: extracted ${fileCount} files, ${formatBytes(byteCount)}`
|
||||
);
|
||||
lastUiUpdate = fileCount;
|
||||
}
|
||||
}
|
||||
|
||||
plugin.settings.lastCommitSha = sha;
|
||||
await plugin.saveSettings();
|
||||
|
||||
notice.setMessage(
|
||||
`Vault Sync: seed complete — ${fileCount} files, ${formatBytes(byteCount)} @ ${sha.slice(0, 7)}`
|
||||
);
|
||||
setTimeout(() => notice.hide(), 6000);
|
||||
}
|
||||
83
src/settings.ts
Normal file
83
src/settings.ts
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
import { App, PluginSettingTab, Setting } from "obsidian";
|
||||
import type VaultSyncPlugin from "./main";
|
||||
|
||||
export interface VaultSyncSettings {
|
||||
pat: string;
|
||||
repoUrl: string;
|
||||
branch: string;
|
||||
lastCommitSha: string;
|
||||
}
|
||||
|
||||
export const DEFAULT_SETTINGS: VaultSyncSettings = {
|
||||
pat: "",
|
||||
repoUrl: "",
|
||||
branch: "main",
|
||||
lastCommitSha: "",
|
||||
};
|
||||
|
||||
export class VaultSyncSettingTab extends PluginSettingTab {
|
||||
plugin: VaultSyncPlugin;
|
||||
|
||||
constructor(app: App, plugin: VaultSyncPlugin) {
|
||||
super(app, plugin);
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
display(): void {
|
||||
const { containerEl } = this;
|
||||
containerEl.empty();
|
||||
|
||||
containerEl.createEl("h2", { text: "Vault Sync (REST)" });
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("GitHub Personal Access Token")
|
||||
.setDesc(
|
||||
"Fine-grained or classic PAT with repo (contents: read/write) scope on the target repo."
|
||||
)
|
||||
.addText((text) => {
|
||||
text.inputEl.type = "password";
|
||||
text.setPlaceholder("ghp_…")
|
||||
.setValue(this.plugin.settings.pat)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.pat = value.trim();
|
||||
await this.plugin.saveSettings();
|
||||
});
|
||||
});
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Repository URL")
|
||||
.setDesc("e.g. https://github.com/owner/repo")
|
||||
.addText((text) =>
|
||||
text
|
||||
.setPlaceholder("https://github.com/owner/repo")
|
||||
.setValue(this.plugin.settings.repoUrl)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.repoUrl = value.trim();
|
||||
await this.plugin.saveSettings();
|
||||
})
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Branch")
|
||||
.setDesc("Branch to sync (default: main)")
|
||||
.addText((text) =>
|
||||
text
|
||||
.setPlaceholder("main")
|
||||
.setValue(this.plugin.settings.branch)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.branch = value.trim() || "main";
|
||||
await this.plugin.saveSettings();
|
||||
})
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Last commit SHA")
|
||||
.setDesc(
|
||||
"Set after a successful seed/pull. Read-only — used as the local sync base."
|
||||
)
|
||||
.addText((text) => {
|
||||
text.setValue(this.plugin.settings.lastCommitSha || "(none)")
|
||||
.setDisabled(true);
|
||||
});
|
||||
}
|
||||
}
|
||||
210
src/tar.ts
Normal file
210
src/tar.ts
Normal file
|
|
@ -0,0 +1,210 @@
|
|||
// Minimal streaming tar parser for the POSIX ustar format produced by GitHub.
|
||||
// Yields { name, type, content } entries from a ReadableStream<Uint8Array>.
|
||||
// No dependencies. Browser-safe.
|
||||
//
|
||||
// Tar layout (USTAR):
|
||||
// bytes 0–100 filename (NUL-terminated, may overflow into prefix)
|
||||
// bytes 100–108 mode
|
||||
// bytes 108–116 uid
|
||||
// bytes 116–124 gid
|
||||
// bytes 124–136 size in octal ASCII (NUL-terminated)
|
||||
// bytes 136–148 mtime
|
||||
// bytes 148–156 checksum
|
||||
// byte 156 typeflag ('0' or '\0' = file, '5' = dir, others ignored v1)
|
||||
// bytes 157–257 linkname
|
||||
// bytes 257–263 ustar magic ("ustar\0")
|
||||
// bytes 265–297 uname
|
||||
// bytes 297–329 gname
|
||||
// bytes 329–337 devmajor
|
||||
// bytes 337–345 devminor
|
||||
// bytes 345–500 prefix (prepended to name with "/" if non-empty)
|
||||
// Header is 512 bytes; data follows, padded up to a 512-byte boundary.
|
||||
// End of archive = two consecutive 512-byte zero blocks.
|
||||
|
||||
export interface TarEntry {
|
||||
name: string;
|
||||
type: "file" | "dir";
|
||||
content: Uint8Array; // empty for dir
|
||||
}
|
||||
|
||||
const BLOCK = 512;
|
||||
|
||||
function readCString(buf: Uint8Array, off: number, len: number): string {
|
||||
let end = off;
|
||||
const lim = off + len;
|
||||
while (end < lim && buf[end] !== 0) end++;
|
||||
return new TextDecoder("utf-8").decode(buf.subarray(off, end));
|
||||
}
|
||||
|
||||
function parseOctal(buf: Uint8Array, off: number, len: number): number {
|
||||
// Trim trailing NUL/space; ignore leading spaces.
|
||||
let s = "";
|
||||
for (let i = off; i < off + len; i++) {
|
||||
const c = buf[i];
|
||||
if (c === 0 || c === 0x20) {
|
||||
if (s.length > 0) break;
|
||||
continue;
|
||||
}
|
||||
s += String.fromCharCode(c);
|
||||
}
|
||||
if (!s) return 0;
|
||||
return parseInt(s, 8) | 0;
|
||||
}
|
||||
|
||||
function isAllZero(buf: Uint8Array, off: number, len: number): boolean {
|
||||
for (let i = off; i < off + len; i++) if (buf[i] !== 0) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* A small append-only buffer that lets us peek at the head and shift consumed
|
||||
* bytes without copying everything every time. We grow by doubling and shift
|
||||
* down only when the consumed prefix exceeds half the buffer.
|
||||
*/
|
||||
class ChunkBuffer {
|
||||
private buf: Uint8Array = new Uint8Array(0);
|
||||
private start = 0; // first valid byte
|
||||
private end = 0; // one past last valid byte
|
||||
|
||||
get length(): number {
|
||||
return this.end - this.start;
|
||||
}
|
||||
|
||||
push(chunk: Uint8Array): void {
|
||||
const need = this.length + chunk.length;
|
||||
if (need > this.buf.length - this.start) {
|
||||
// Either grow or shift to make room.
|
||||
if (this.start > 0 && need <= this.buf.length) {
|
||||
this.buf.copyWithin(0, this.start, this.end);
|
||||
this.end -= this.start;
|
||||
this.start = 0;
|
||||
} else {
|
||||
let cap = Math.max(this.buf.length * 2, 64 * 1024);
|
||||
while (cap < need) cap *= 2;
|
||||
const next = new Uint8Array(cap);
|
||||
next.set(this.buf.subarray(this.start, this.end), 0);
|
||||
this.end -= this.start;
|
||||
this.start = 0;
|
||||
this.buf = next;
|
||||
}
|
||||
}
|
||||
this.buf.set(chunk, this.end);
|
||||
this.end += chunk.length;
|
||||
}
|
||||
|
||||
/** Returns a view of the next `n` bytes without consuming. Caller must ensure length >= n. */
|
||||
peek(n: number): Uint8Array {
|
||||
return this.buf.subarray(this.start, this.start + n);
|
||||
}
|
||||
|
||||
/** Consume `n` bytes, returning a copy (safe to keep). */
|
||||
take(n: number): Uint8Array {
|
||||
const out = this.buf.slice(this.start, this.start + n);
|
||||
this.start += n;
|
||||
if (this.start > this.buf.length / 2) {
|
||||
// Compact occasionally to bound memory.
|
||||
this.buf.copyWithin(0, this.start, this.end);
|
||||
this.end -= this.start;
|
||||
this.start = 0;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** Discard `n` bytes (e.g. padding). */
|
||||
skip(n: number): void {
|
||||
this.start += n;
|
||||
if (this.start > this.buf.length / 2) {
|
||||
this.buf.copyWithin(0, this.start, this.end);
|
||||
this.end -= this.start;
|
||||
this.start = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a tar stream and yield entries. The stream is assumed uncompressed
|
||||
* (gzip decompression must happen upstream via DecompressionStream).
|
||||
*/
|
||||
export async function* parseTar(
|
||||
stream: ReadableStream<Uint8Array>
|
||||
): AsyncGenerator<TarEntry, void, void> {
|
||||
const reader = stream.getReader();
|
||||
const buf = new ChunkBuffer();
|
||||
let zeroBlocks = 0;
|
||||
let done = false;
|
||||
|
||||
async function fillTo(n: number): Promise<boolean> {
|
||||
while (buf.length < n) {
|
||||
if (done) return false;
|
||||
const { value, done: d } = await reader.read();
|
||||
if (d) {
|
||||
done = true;
|
||||
return buf.length >= n;
|
||||
}
|
||||
if (value && value.length) buf.push(value);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
while (true) {
|
||||
if (!(await fillTo(BLOCK))) break;
|
||||
|
||||
const header = buf.peek(BLOCK);
|
||||
if (isAllZero(header, 0, BLOCK)) {
|
||||
buf.skip(BLOCK);
|
||||
zeroBlocks++;
|
||||
if (zeroBlocks >= 2) break; // end-of-archive marker
|
||||
continue;
|
||||
}
|
||||
zeroBlocks = 0;
|
||||
|
||||
const namePart = readCString(header, 0, 100);
|
||||
const size = parseOctal(header, 124, 12);
|
||||
const typeflagByte = header[156];
|
||||
const typeflag = typeflagByte === 0 ? "0" : String.fromCharCode(typeflagByte);
|
||||
|
||||
// ustar prefix for long names.
|
||||
const ustar = readCString(header, 257, 6);
|
||||
let prefix = "";
|
||||
if (ustar === "ustar" || ustar.startsWith("ustar")) {
|
||||
prefix = readCString(header, 345, 155);
|
||||
}
|
||||
const name = prefix ? `${prefix}/${namePart}` : namePart;
|
||||
|
||||
buf.skip(BLOCK); // header consumed
|
||||
|
||||
const padded = size > 0 ? Math.ceil(size / BLOCK) * BLOCK : 0;
|
||||
|
||||
// Skip non-file/dir entries (long-link records, symlinks, etc.).
|
||||
if (typeflag !== "0" && typeflag !== "5" && typeflag !== "\0") {
|
||||
// Drain the data + padding without keeping it.
|
||||
let remaining = padded;
|
||||
while (remaining > 0) {
|
||||
if (!(await fillTo(Math.min(remaining, BLOCK)))) {
|
||||
return; // truncated
|
||||
}
|
||||
const take = Math.min(remaining, buf.length);
|
||||
buf.skip(take);
|
||||
remaining -= take;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (typeflag === "5") {
|
||||
// Directory entry — no data, but padded is 0 anyway.
|
||||
yield { name, type: "dir", content: new Uint8Array(0) };
|
||||
continue;
|
||||
}
|
||||
|
||||
// File entry: read `size` bytes (and skip padding to next block boundary).
|
||||
if (!(await fillTo(padded))) {
|
||||
// Truncated file — best-effort: yield what we have, then stop.
|
||||
const have = Math.min(buf.length, size);
|
||||
yield { name, type: "file", content: buf.take(have) };
|
||||
return;
|
||||
}
|
||||
const content = buf.take(size);
|
||||
if (padded > size) buf.skip(padded - size);
|
||||
yield { name, type: "file", content };
|
||||
}
|
||||
}
|
||||
1
styles.css
Normal file
1
styles.css
Normal file
|
|
@ -0,0 +1 @@
|
|||
/* Vault Sync (REST) styles - placeholder for Phase 1 */
|
||||
26
tsconfig.json
Normal file
26
tsconfig.json
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"inlineSourceMap": true,
|
||||
"inlineSources": true,
|
||||
"module": "ESNext",
|
||||
"target": "ES2020",
|
||||
"allowJs": true,
|
||||
"noImplicitAny": true,
|
||||
"strict": true,
|
||||
"isolatedModules": true,
|
||||
"moduleResolution": "node",
|
||||
"importHelpers": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"lib": [
|
||||
"DOM",
|
||||
"ES2020",
|
||||
"ES2021.WeakRef"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts"
|
||||
]
|
||||
}
|
||||
Loading…
Reference in a new issue