You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
218 lines
6.0 KiB
TypeScript
218 lines
6.0 KiB
TypeScript
import { consola } from 'consola';
|
|
import { writeJSONSync } from 'fs-extra';
|
|
import matter from 'gray-matter';
|
|
import { createHash } from 'node:crypto';
|
|
import { readFileSync, writeFileSync } from 'node:fs';
|
|
import { resolve } from 'node:path';
|
|
import pMap from 'p-map';
|
|
|
|
import { uploader } from './uploader';
|
|
import {
|
|
changelogIndex,
|
|
changelogIndexPath,
|
|
extractHttpsLinks,
|
|
fetchImageAsFile,
|
|
mergeAndDeduplicateArrays,
|
|
posts,
|
|
root,
|
|
} from './utils';
|
|
|
|
// 定义常量
|
|
const GITHUB_CDN = 'https://github.com/lobehub/lobe-chat/assets/';
|
|
const CHECK_CDN = [
|
|
'https://cdn.nlark.com/yuque/0/',
|
|
'https://s.imtccdn.com/',
|
|
'https://oss.home.imtc.top/',
|
|
'https://www.anthropic.com/_next/image',
|
|
'https://miro.medium.com/v2/',
|
|
'https://images.unsplash.com/',
|
|
'https://github.com/user-attachments/assets',
|
|
];
|
|
|
|
const CACHE_FILE = resolve(root, 'docs', '.cdn.cache.json');
|
|
|
|
class ImageCDNUploader {
|
|
private cache: { [link: string]: string } = {};
|
|
|
|
constructor() {
|
|
this.loadCache();
|
|
}
|
|
|
|
// 从文件加载缓存数据
|
|
private loadCache() {
|
|
try {
|
|
this.cache = JSON.parse(readFileSync(CACHE_FILE, 'utf8'));
|
|
} catch (error) {
|
|
consola.error('Failed to load cache', error);
|
|
}
|
|
}
|
|
|
|
// 将缓存数据写入文件
|
|
private writeCache() {
|
|
try {
|
|
writeFileSync(CACHE_FILE, JSON.stringify(this.cache, null, 2));
|
|
} catch (error) {
|
|
consola.error('Failed to write cache', error);
|
|
}
|
|
}
|
|
|
|
// 收集所有的图片链接
|
|
private collectImageLinks(): string[] {
|
|
const links: string[][] = posts.map((post) => {
|
|
const mdx = readFileSync(post, 'utf8');
|
|
const { content, data } = matter(mdx);
|
|
let inlineLinks: string[] = extractHttpsLinks(content);
|
|
|
|
// 添加特定字段中的图片链接
|
|
if (data?.image) inlineLinks.push(data.image);
|
|
if (data?.seo?.image) inlineLinks.push(data.seo.image);
|
|
|
|
// 过滤出有效的 CDN 链接
|
|
return inlineLinks.filter(
|
|
(link) =>
|
|
(link.startsWith(GITHUB_CDN) || CHECK_CDN.some((cdn) => link.startsWith(cdn))) &&
|
|
!this.cache[link],
|
|
);
|
|
});
|
|
|
|
const communityLinks = changelogIndex.community
|
|
.map((post) => post.image)
|
|
.filter(
|
|
(link) =>
|
|
link &&
|
|
(link.startsWith(GITHUB_CDN) || CHECK_CDN.some((cdn) => link.startsWith(cdn))) &&
|
|
!this.cache[link],
|
|
) as string[];
|
|
|
|
const cloudLinks = changelogIndex.cloud
|
|
.map((post) => post.image)
|
|
.filter(
|
|
(link) =>
|
|
link &&
|
|
(link.startsWith(GITHUB_CDN) || CHECK_CDN.some((cdn) => link.startsWith(cdn))) &&
|
|
!this.cache[link],
|
|
) as string[];
|
|
|
|
// 合并和去重链接数组
|
|
return mergeAndDeduplicateArrays(links.flat().concat(communityLinks, cloudLinks));
|
|
}
|
|
|
|
// 上传图片到 CDN
|
|
private async uploadImagesToCDN(links: string[]) {
|
|
const cdnLinks: { [link: string]: string } = {};
|
|
|
|
await pMap(links, async (link) => {
|
|
consola.start('Uploading image to CDN', link);
|
|
const file = await fetchImageAsFile(link, 1600);
|
|
|
|
if (!file) {
|
|
consola.error('Failed to fetch image as file', link);
|
|
return;
|
|
}
|
|
|
|
const cdnUrl = await this.uploadFileToCDN(file, link);
|
|
if (cdnUrl) {
|
|
consola.success(link, '>>>', cdnUrl);
|
|
cdnLinks[link] = cdnUrl.replaceAll(process.env.DOC_S3_PUBLIC_DOMAIN || '', '');
|
|
}
|
|
});
|
|
|
|
// 更新缓存
|
|
this.cache = { ...this.cache, ...cdnLinks };
|
|
this.writeCache();
|
|
}
|
|
|
|
// 根据不同的 CDN 来处理文件上传
|
|
private async uploadFileToCDN(file: File, link: string): Promise<string | undefined> {
|
|
if (link.startsWith(GITHUB_CDN)) {
|
|
const filename = link.replaceAll(GITHUB_CDN, '');
|
|
return uploader(file, filename);
|
|
} else if (CHECK_CDN.some((cdn) => link.startsWith(cdn))) {
|
|
const buffer = await file.arrayBuffer();
|
|
const hash = createHash('md5').update(Buffer.from(buffer)).digest('hex');
|
|
return uploader(file, hash);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
// 替换文章中的图片链接
|
|
private replaceLinksInPosts() {
|
|
let count = 0;
|
|
|
|
for (const post of posts) {
|
|
const mdx = readFileSync(post, 'utf8');
|
|
let { content, data } = matter(mdx);
|
|
const inlineLinks = extractHttpsLinks(content);
|
|
|
|
for (const link of inlineLinks) {
|
|
if (this.cache[link]) {
|
|
content = content.replaceAll(link, this.cache[link]);
|
|
count++;
|
|
}
|
|
}
|
|
|
|
// 更新特定字段的图片链接
|
|
|
|
if (data['image'] && this.cache[data['image']]) {
|
|
data['image'] = this.cache[data['image']];
|
|
count++;
|
|
}
|
|
|
|
if (data['seo']?.['image'] && this.cache[data['seo']?.['image']]) {
|
|
data['seo']['image'] = this.cache[data['seo']['image']];
|
|
count++;
|
|
}
|
|
|
|
writeFileSync(post, matter.stringify(content, data));
|
|
}
|
|
|
|
consola.success(`${count} images have been uploaded to CDN and links have been replaced`);
|
|
}
|
|
|
|
private replaceLinksInChangelogIndex() {
|
|
let count = 0;
|
|
changelogIndex.community = changelogIndex.community.map((post) => {
|
|
if (!post.image) return post;
|
|
count++;
|
|
return {
|
|
...post,
|
|
image: this.cache[post.image] || post.image,
|
|
};
|
|
});
|
|
|
|
changelogIndex.cloud = changelogIndex.cloud.map((post) => {
|
|
if (!post.image) return post;
|
|
count++;
|
|
return {
|
|
...post,
|
|
image: this.cache[post.image] || post.image,
|
|
};
|
|
});
|
|
|
|
writeJSONSync(changelogIndexPath, changelogIndex, { spaces: 2 });
|
|
|
|
consola.success(
|
|
`${count} changelog index images have been uploaded to CDN and links have been replaced`,
|
|
);
|
|
}
|
|
|
|
// 运行上传过程
|
|
async run() {
|
|
const links = this.collectImageLinks();
|
|
|
|
if (links.length > 0) {
|
|
consola.info("Found images that haven't been uploaded to CDN:");
|
|
consola.info(links);
|
|
await this.uploadImagesToCDN(links);
|
|
} else {
|
|
consola.info('No new images to upload.');
|
|
}
|
|
}
|
|
}
|
|
|
|
// 实例化并运行
|
|
const instance = new ImageCDNUploader();
|
|
|
|
instance.run();
|