4186647222
notebook-ci / build-push-deploy (push) Successful in 49s
Summarize the Typecho XML-RPC to RCE to root attack chain as a publishable security note, and add a small WebDAV PUT script to sync individual notes to Jianguoyun KnowledgeBase.
134 lines
4.2 KiB
JavaScript
134 lines
4.2 KiB
JavaScript
/**
|
|
* 将本地一篇(或多篇)笔记 PUT 到坚果云 WebDAV
|
|
* 用法: node scripts/push-to-nutstore.mjs <local-md-path> [remote-rel-path]
|
|
*
|
|
* remote-rel-path 相对 NUTSTORE_WEBDAV_PATH,默认与 posts 下相对路径一致
|
|
*/
|
|
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { loadEnv } from './lib/load-env.mjs';
|
|
|
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
const root = path.resolve(__dirname, '..');
|
|
loadEnv(root);
|
|
|
|
const localArg = process.argv[2];
|
|
if (!localArg) {
|
|
console.error('用法: node scripts/push-to-nutstore.mjs <local-md-path> [remote-rel-path]');
|
|
process.exit(1);
|
|
}
|
|
|
|
const localPath = path.isAbsolute(localArg) ? localArg : path.join(root, localArg);
|
|
if (!fs.existsSync(localPath)) {
|
|
console.error('文件不存在:', localPath);
|
|
process.exit(1);
|
|
}
|
|
|
|
const postsOut = path.join(root, 'src/content/posts');
|
|
let remoteRel = process.argv[3];
|
|
if (!remoteRel) {
|
|
const rel = path.relative(postsOut, localPath);
|
|
if (rel.startsWith('..')) {
|
|
remoteRel = path.basename(localPath);
|
|
} else {
|
|
remoteRel = rel.split(path.sep).join('/');
|
|
}
|
|
}
|
|
|
|
const DAV_BASE = (process.env.NUTSTORE_WEBDAV_URL || 'https://dav.jianguoyun.com/dav').replace(
|
|
/\/$/,
|
|
'',
|
|
);
|
|
const sub = (process.env.NUTSTORE_WEBDAV_PATH || '').replace(/^\/dav\/?/i, '').replace(/^\/+|\/+$/g, '');
|
|
const user = process.env.NUTSTORE_WEBDAV_USER || process.env.NUTSTORE_USER;
|
|
const pass = process.env.NUTSTORE_WEBDAV_PASS || process.env.NUTSTORE_PASS;
|
|
if (!user || !pass) {
|
|
console.error('缺少 NUTSTORE_WEBDAV_USER / NUTSTORE_WEBDAV_PASS');
|
|
process.exit(1);
|
|
}
|
|
|
|
const auth = 'Basic ' + Buffer.from(`${user}:${pass}`).toString('base64');
|
|
|
|
function joinDav(...parts) {
|
|
return parts
|
|
.filter(Boolean)
|
|
.map((p, i) => (i === 0 ? p.replace(/\/$/, '') : String(p).replace(/^\/+|\/+$/g, '')))
|
|
.join('/');
|
|
}
|
|
|
|
async function ensureDir(davDirUrl) {
|
|
// MKCOL each segment under base
|
|
const base = joinDav(DAV_BASE, sub);
|
|
if (!davDirUrl.startsWith(base)) return;
|
|
let rel = davDirUrl.slice(base.length).replace(/^\/+/, '');
|
|
if (!rel) return;
|
|
const segs = rel.split('/').filter(Boolean);
|
|
let cur = base;
|
|
for (const seg of segs) {
|
|
cur = joinDav(cur, encodeURIComponent(seg).replace(/%2F/gi, '/'));
|
|
// try without encode for chinese paths - 坚果云 often wants raw utf8 path encoded per segment
|
|
const tryUrl = joinDav(base, ...segs.slice(0, segs.indexOf(seg) + 1).map(encodeURIComponent));
|
|
const res = await fetch(tryUrl + (tryUrl.endsWith('/') ? '' : '/'), {
|
|
method: 'MKCOL',
|
|
headers: { Authorization: auth },
|
|
});
|
|
// 201 created, 405 exists, 409 parent missing handled by order
|
|
if (![201, 405, 301, 200].includes(res.status) && res.status !== 409) {
|
|
// 409 can mean exists as collection on some servers
|
|
if (res.status !== 409) {
|
|
const t = await res.text().catch(() => '');
|
|
// ignore "already exists" style
|
|
if (res.status !== 405) {
|
|
console.warn(`MKCOL ${res.status} ${tryUrl} ${t.slice(0, 120)}`);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
async function mkcolRecursive(remoteRelPath) {
|
|
const parts = remoteRelPath.split('/').filter(Boolean);
|
|
// only dirs
|
|
const dirParts = parts.slice(0, -1);
|
|
let built = [];
|
|
for (const p of dirParts) {
|
|
built.push(p);
|
|
const url =
|
|
joinDav(DAV_BASE, sub, ...built.map((s) => encodeURIComponent(s))) + '/';
|
|
const res = await fetch(url, { method: 'MKCOL', headers: { Authorization: auth } });
|
|
console.log(`MKCOL ${res.status} ${decodeURIComponent(url)}`);
|
|
}
|
|
}
|
|
|
|
async function main() {
|
|
const fileName = path.posix.basename(remoteRel.split(path.sep).join('/'));
|
|
const remotePosix = remoteRel.split(path.sep).join('/');
|
|
await mkcolRecursive(remotePosix);
|
|
|
|
const url =
|
|
joinDav(DAV_BASE, sub, ...remotePosix.split('/').map(encodeURIComponent));
|
|
const body = fs.readFileSync(localPath);
|
|
const res = await fetch(url, {
|
|
method: 'PUT',
|
|
headers: {
|
|
Authorization: auth,
|
|
'Content-Type': 'text/markdown; charset=utf-8',
|
|
'Content-Length': String(body.length),
|
|
},
|
|
body,
|
|
});
|
|
const t = await res.text().catch(() => '');
|
|
console.log(`PUT ${res.status} ${decodeURIComponent(url)}`);
|
|
if (!res.ok) {
|
|
console.error(t.slice(0, 400));
|
|
process.exit(1);
|
|
}
|
|
console.log('已同步到坚果云:', path.posix.join(sub, remotePosix));
|
|
}
|
|
|
|
main().catch((e) => {
|
|
console.error(e);
|
|
process.exit(1);
|
|
});
|