139 lines
3.6 KiB
JavaScript
139 lines
3.6 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* 清空 CalDAV 日历事件
|
|
*/
|
|
|
|
import https from 'https';
|
|
|
|
const CONFIG = {
|
|
url: 'https://micecola.top:5001/caldav/zhangqing/home/',
|
|
user: 'zhangqing',
|
|
password: 'MiceCola1265'
|
|
};
|
|
|
|
function log(level, message) {
|
|
const timestamp = new Date().toISOString().replace('T', ' ').slice(0, 19);
|
|
console.log(`${timestamp} - ${level} - ${message}`);
|
|
}
|
|
|
|
async function request(method, path = '', body = null) {
|
|
return new Promise((resolve, reject) => {
|
|
const url = new URL(CONFIG.url + path);
|
|
|
|
const options = {
|
|
hostname: url.hostname,
|
|
port: url.port || 443,
|
|
path: url.pathname,
|
|
method: method,
|
|
headers: {
|
|
'Authorization': 'Basic ' + Buffer.from(`${CONFIG.user}:${CONFIG.password}`).toString('base64'),
|
|
'Content-Type': 'application/xml; charset=utf-8',
|
|
'Depth': 'infinity'
|
|
},
|
|
rejectUnauthorized: false
|
|
};
|
|
|
|
const req = https.request(options, (res) => {
|
|
let data = '';
|
|
res.on('data', chunk => data += chunk);
|
|
res.on('end', () => {
|
|
if (res.statusCode >= 200 && res.statusCode < 300) {
|
|
resolve({ status: res.statusCode, data, headers: res.headers });
|
|
} else {
|
|
resolve({ status: res.statusCode, data, headers: res.headers });
|
|
}
|
|
});
|
|
});
|
|
|
|
req.on('error', reject);
|
|
if (body) req.write(body);
|
|
req.end();
|
|
});
|
|
}
|
|
|
|
async function getAllEvents() {
|
|
// 使用 PROPFIND 获取日历内容
|
|
const response = await request('PROPFIND', '', `<?xml version="1.0" encoding="utf-8"?>
|
|
<d:propfind xmlns:d="DAV:">
|
|
<d:prop>
|
|
<d:resourcetype />
|
|
<d:getcontenttype />
|
|
</d:prop>
|
|
</d:propfind>`);
|
|
|
|
// 提取所有 .ics 文件的 href(可能在 <href> 或 <propstat><prop><href> 中)
|
|
const hrefs = [];
|
|
|
|
// 匹配所有 href,排除目录
|
|
const allHrefs = response.data.match(/<href>([^<]+)<\/href>/gi) || [];
|
|
for (const match of allHrefs) {
|
|
const href = match.replace(/<\/?href>/gi, '');
|
|
// 只保留 .ics 文件,排除目录
|
|
if (href.endsWith('.ics')) {
|
|
// 解码 URL 编码
|
|
const decodedHref = decodeURIComponent(href);
|
|
if (!hrefs.includes(decodedHref)) {
|
|
hrefs.push(decodedHref);
|
|
}
|
|
}
|
|
}
|
|
|
|
return hrefs;
|
|
}
|
|
|
|
async function deleteEvent(href) {
|
|
return new Promise((resolve, reject) => {
|
|
// href 是相对路径,需要用完整 URL
|
|
const baseUrl = new URL(CONFIG.url);
|
|
const fullPath = href.startsWith('/') ? href : '/' + href;
|
|
|
|
const options = {
|
|
hostname: baseUrl.hostname,
|
|
port: baseUrl.port || 443,
|
|
path: fullPath,
|
|
method: 'DELETE',
|
|
headers: {
|
|
'Authorization': 'Basic ' + Buffer.from(`${CONFIG.user}:${CONFIG.password}`).toString('base64')
|
|
},
|
|
rejectUnauthorized: false
|
|
};
|
|
|
|
const req = https.request(options, (res) => {
|
|
let data = '';
|
|
res.on('data', chunk => data += chunk);
|
|
res.on('end', () => resolve(res.statusCode));
|
|
});
|
|
|
|
req.on('error', reject);
|
|
req.end();
|
|
});
|
|
}
|
|
|
|
async function main() {
|
|
log('INFO', '获取日历事件列表...');
|
|
|
|
const hrefs = await getAllEvents();
|
|
log('INFO', `找到 ${hrefs.length} 个事件`);
|
|
|
|
let deleted = 0;
|
|
let failed = 0;
|
|
|
|
for (const href of hrefs) {
|
|
const status = await deleteEvent(href);
|
|
if (status >= 200 && status < 300) {
|
|
deleted++;
|
|
log('INFO', `已删除: ${href.split('/').pop()}`);
|
|
} else {
|
|
failed++;
|
|
log('ERROR', `删除失败 (${status}): ${href}`);
|
|
}
|
|
}
|
|
|
|
log('INFO', `完成: 删除 ${deleted} 个,失败 ${failed} 个`);
|
|
}
|
|
|
|
main().catch(e => {
|
|
log('ERROR', `程序异常: ${e.message}`);
|
|
process.exit(1);
|
|
});
|