200 lines
5.8 KiB
TypeScript
200 lines
5.8 KiB
TypeScript
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
import path from 'node:path';
|
|
import process from 'node:process';
|
|
import { fetchGiteaSnapshot, type GeneratedGiteaActivity, type GeneratedProject } from './fetch-gitea.ts';
|
|
import { fetchSeafileSnapshot, type GeneratedShare } from './fetch-seafile.ts';
|
|
|
|
type ProjectSeed = {
|
|
name: string;
|
|
description: string;
|
|
gitea_repo: string;
|
|
cover_image: string;
|
|
demo_video?: string;
|
|
download_link?: string;
|
|
tags: string[];
|
|
featured: boolean;
|
|
};
|
|
|
|
type ShareSeed = {
|
|
name: string;
|
|
description?: string;
|
|
url?: string;
|
|
time?: string;
|
|
};
|
|
|
|
type SyncConfig = {
|
|
rootDir: string;
|
|
outputDir: string;
|
|
strictSync: boolean;
|
|
gitea: {
|
|
baseUrl: string;
|
|
token: string;
|
|
username: string;
|
|
activityDays: number;
|
|
activityPerDayLimit: number;
|
|
requestConcurrency: number;
|
|
recentItemLimit: number;
|
|
requestTimeoutMs: number;
|
|
};
|
|
seafile: {
|
|
baseUrl: string;
|
|
token: string;
|
|
mirrorDownloads: boolean;
|
|
downloadsOutputDir: string;
|
|
requestTimeoutMs: number;
|
|
};
|
|
};
|
|
|
|
async function main() {
|
|
const config = loadConfig(process.cwd());
|
|
const syncedAt = new Date().toISOString();
|
|
|
|
console.log('[sync-content] start');
|
|
console.log(`[sync-content] output dir: ${config.outputDir}`);
|
|
|
|
await mkdir(config.outputDir, { recursive: true });
|
|
|
|
if (config.seafile.mirrorDownloads) {
|
|
console.warn(
|
|
'[sync-content] SEAFILE_MIRROR_DOWNLOADS=true, but file mirroring is not implemented yet.',
|
|
);
|
|
}
|
|
|
|
const seedProjects = await readJson<ProjectSeed[]>(
|
|
path.join(config.rootDir, 'src/content/projects/index.json'),
|
|
);
|
|
const seedShares = await readJson<ShareSeed[]>(
|
|
path.join(config.rootDir, 'src/content/shares/index.json'),
|
|
);
|
|
|
|
const giteaSnapshot = hasGiteaConfig(config)
|
|
? await fetchGiteaSnapshot({
|
|
config: {
|
|
...config.gitea,
|
|
strict: config.strictSync,
|
|
},
|
|
projectSeeds: seedProjects,
|
|
syncedAt,
|
|
})
|
|
: null;
|
|
|
|
const baseProjects =
|
|
giteaSnapshot?.projects ??
|
|
seedProjects.map((project) => ({
|
|
...project,
|
|
repo_url:
|
|
project.gitea_repo && config.gitea.baseUrl
|
|
? `${trimTrailingSlash(config.gitea.baseUrl)}/${project.gitea_repo}`
|
|
: undefined,
|
|
updated_at: syncedAt,
|
|
source: 'seed' as const,
|
|
}));
|
|
|
|
const seafileSnapshot = await fetchSeafileSnapshot({
|
|
config: {
|
|
...config.seafile,
|
|
strict: config.strictSync,
|
|
},
|
|
projects: baseProjects,
|
|
seedShares,
|
|
syncedAt,
|
|
mappingFilePath: path.join(config.rootDir, 'src/content/seafile/index.json'),
|
|
});
|
|
|
|
const giteaActivity =
|
|
giteaSnapshot?.activity ??
|
|
({
|
|
updatedAt: syncedAt,
|
|
source: 'placeholder',
|
|
itemCount: 0,
|
|
days: [],
|
|
recent: [],
|
|
} satisfies GeneratedGiteaActivity);
|
|
|
|
await Promise.all([
|
|
writeJson(path.join(config.outputDir, 'projects.json'), seafileSnapshot.projects),
|
|
writeJson(path.join(config.outputDir, 'shares.json'), seafileSnapshot.shares),
|
|
writeJson(path.join(config.outputDir, 'gitea-activity.json'), giteaActivity),
|
|
]);
|
|
|
|
console.log(`[sync-content] wrote ${seafileSnapshot.projects.length} projects`);
|
|
console.log(`[sync-content] wrote ${seafileSnapshot.shares.length} shares`);
|
|
console.log(`[sync-content] wrote ${giteaActivity.itemCount} activity items`);
|
|
console.log('[sync-content] done');
|
|
}
|
|
|
|
function loadConfig(rootDir: string): SyncConfig {
|
|
return {
|
|
rootDir,
|
|
outputDir: resolveFromRoot(rootDir, process.env.SYNC_OUTPUT_DIR ?? 'src/data/generated'),
|
|
strictSync: getBooleanEnv('STRICT_SYNC', false),
|
|
gitea: {
|
|
baseUrl: process.env.GITEA_BASE_URL?.trim() ?? '',
|
|
token: process.env.GITEA_TOKEN?.trim() ?? '',
|
|
username: process.env.GITEA_USERNAME?.trim() ?? '',
|
|
activityDays: getNumberEnv('GITEA_ACTIVITY_DAYS', 70),
|
|
activityPerDayLimit: getNumberEnv('GITEA_ACTIVITY_PER_DAY_LIMIT', 50),
|
|
recentItemLimit: getNumberEnv('GITEA_RECENT_ITEM_LIMIT', 8),
|
|
requestTimeoutMs: getNumberEnv('GITEA_REQUEST_TIMEOUT_MS', 15000),
|
|
requestConcurrency: getNumberEnv('GITEA_REQUEST_CONCURRENCY', 5),
|
|
},
|
|
seafile: {
|
|
baseUrl: process.env.SEAFILE_BASE_URL?.trim() ?? '',
|
|
token: process.env.SEAFILE_TOKEN?.trim() ?? '',
|
|
mirrorDownloads: getBooleanEnv('SEAFILE_MIRROR_DOWNLOADS', false),
|
|
downloadsOutputDir: resolveFromRoot(
|
|
rootDir,
|
|
process.env.DOWNLOADS_OUTPUT_DIR ?? 'public/downloads',
|
|
),
|
|
requestTimeoutMs: getNumberEnv('SEAFILE_REQUEST_TIMEOUT_MS', 15000),
|
|
},
|
|
};
|
|
}
|
|
|
|
function hasGiteaConfig(config: SyncConfig) {
|
|
return Boolean(config.gitea.baseUrl && config.gitea.token && config.gitea.username);
|
|
}
|
|
|
|
function getBooleanEnv(name: string, fallback: boolean) {
|
|
const value = process.env[name];
|
|
if (value == null || value.trim() === '') {
|
|
return fallback;
|
|
}
|
|
|
|
return ['1', 'true', 'yes', 'on'].includes(value.trim().toLowerCase());
|
|
}
|
|
|
|
function resolveFromRoot(rootDir: string, targetPath: string) {
|
|
return path.isAbsolute(targetPath) ? targetPath : path.join(rootDir, targetPath);
|
|
}
|
|
|
|
function trimTrailingSlash(value: string) {
|
|
return value.replace(/\/+$/, '');
|
|
}
|
|
|
|
function getNumberEnv(name: string, fallback: number) {
|
|
const value = process.env[name];
|
|
if (value == null || value.trim() === '') {
|
|
return fallback;
|
|
}
|
|
|
|
const parsed = Number(value);
|
|
return Number.isFinite(parsed) ? parsed : fallback;
|
|
}
|
|
|
|
async function readJson<T>(filePath: string): Promise<T> {
|
|
const raw = await readFile(filePath, 'utf-8');
|
|
return JSON.parse(raw) as T;
|
|
}
|
|
|
|
async function writeJson(filePath: string, data: unknown) {
|
|
await mkdir(path.dirname(filePath), { recursive: true });
|
|
await writeFile(filePath, `${JSON.stringify(data, null, 2)}\n`, 'utf-8');
|
|
}
|
|
|
|
main().catch((error) => {
|
|
console.error('[sync-content] failed');
|
|
console.error(error);
|
|
process.exitCode = 1;
|
|
});
|