2024-12-26 03:02:47 +00:00
|
|
|
import { createHash } from 'node:crypto';
|
|
|
|
|
import { readFileSync, writeFileSync } from 'node:fs';
|
|
|
|
|
import { resolve } from 'node:path';
|
2026-02-05 13:40:43 +00:00
|
|
|
|
|
|
|
|
import { consola } from 'consola';
|
|
|
|
|
import { writeJSONSync } from 'fs-extra';
|
|
|
|
|
import matter from 'gray-matter';
|
2024-12-26 03:02:47 +00:00
|
|
|
import pMap from 'p-map';
|
|
|
|
|
|
|
|
|
|
import { uploader } from './uploader';
|
|
|
|
|
import {
|
|
|
|
|
changelogIndex,
|
|
|
|
|
changelogIndexPath,
|
|
|
|
|
extractHttpsLinks,
|
|
|
|
|
fetchImageAsFile,
|
|
|
|
|
mergeAndDeduplicateArrays,
|
|
|
|
|
posts,
|
|
|
|
|
root,
|
|
|
|
|
} from './utils';
|
|
|
|
|
|
2026-04-09 06:38:29 +00:00
|
|
|
// Define constants
|
2024-12-26 03:02:47 +00:00
|
|
|
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',
|
2026-01-26 07:28:33 +00:00
|
|
|
'https://i.imgur.com/',
|
|
|
|
|
'https://file.rene.wang',
|
2024-12-26 03:02:47 +00:00
|
|
|
];
|
|
|
|
|
|
|
|
|
|
const CACHE_FILE = resolve(root, 'docs', '.cdn.cache.json');
|
|
|
|
|
|
|
|
|
|
class ImageCDNUploader {
|
|
|
|
|
private cache: { [link: string]: string } = {};
|
|
|
|
|
|
|
|
|
|
constructor() {
|
|
|
|
|
this.loadCache();
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-09 06:38:29 +00:00
|
|
|
// Load cache data from file
|
2024-12-26 03:02:47 +00:00
|
|
|
private loadCache() {
|
|
|
|
|
try {
|
|
|
|
|
this.cache = JSON.parse(readFileSync(CACHE_FILE, 'utf8'));
|
|
|
|
|
} catch (error) {
|
|
|
|
|
consola.error('Failed to load cache', error);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-09 06:38:29 +00:00
|
|
|
// Write cache data to file
|
2024-12-26 03:02:47 +00:00
|
|
|
private writeCache() {
|
|
|
|
|
try {
|
|
|
|
|
writeFileSync(CACHE_FILE, JSON.stringify(this.cache, null, 2));
|
|
|
|
|
} catch (error) {
|
|
|
|
|
consola.error('Failed to write cache', error);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-09 06:38:29 +00:00
|
|
|
// Collect all image links
|
2024-12-26 03:02:47 +00:00
|
|
|
private collectImageLinks(): string[] {
|
|
|
|
|
const links: string[][] = posts.map((post) => {
|
|
|
|
|
const mdx = readFileSync(post, 'utf8');
|
|
|
|
|
const { content, data } = matter(mdx);
|
2026-02-05 13:40:43 +00:00
|
|
|
const inlineLinks: string[] = extractHttpsLinks(content);
|
2024-12-26 03:02:47 +00:00
|
|
|
|
2026-04-09 06:38:29 +00:00
|
|
|
// Add image links from specific fields
|
2024-12-26 03:02:47 +00:00
|
|
|
if (data?.image) inlineLinks.push(data.image);
|
|
|
|
|
if (data?.seo?.image) inlineLinks.push(data.seo.image);
|
|
|
|
|
|
2026-04-09 06:38:29 +00:00
|
|
|
// Filter out valid CDN links
|
2024-12-26 03:02:47 +00:00
|
|
|
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[];
|
|
|
|
|
|
2026-04-09 06:38:29 +00:00
|
|
|
// Merge and deduplicate link arrays
|
2024-12-26 03:02:47 +00:00
|
|
|
return mergeAndDeduplicateArrays(links.flat().concat(communityLinks, cloudLinks));
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-09 06:38:29 +00:00
|
|
|
// Upload images to CDN
|
2024-12-26 03:02:47 +00:00
|
|
|
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 || '', '');
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
2026-04-09 06:38:29 +00:00
|
|
|
// Update cache
|
2024-12-26 03:02:47 +00:00
|
|
|
this.cache = { ...this.cache, ...cdnLinks };
|
|
|
|
|
this.writeCache();
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-09 06:38:29 +00:00
|
|
|
// Handle file upload based on CDN type
|
2024-12-26 03:02:47 +00:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-09 06:38:29 +00:00
|
|
|
// Replace image links in posts
|
2024-12-26 03:02:47 +00:00
|
|
|
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++;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-09 06:38:29 +00:00
|
|
|
// Update image links in specific fields
|
2024-12-26 03:02:47 +00:00
|
|
|
|
|
|
|
|
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;
|
2026-01-27 03:51:12 +00:00
|
|
|
if (this.cache[post.image]) {
|
|
|
|
|
count++;
|
|
|
|
|
return {
|
|
|
|
|
...post,
|
|
|
|
|
image: this.cache[post.image],
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
return post;
|
2024-12-26 03:02:47 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
changelogIndex.cloud = changelogIndex.cloud.map((post) => {
|
|
|
|
|
if (!post.image) return post;
|
2026-01-27 03:51:12 +00:00
|
|
|
if (this.cache[post.image]) {
|
|
|
|
|
count++;
|
|
|
|
|
return {
|
|
|
|
|
...post,
|
|
|
|
|
image: this.cache[post.image],
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
return post;
|
2024-12-26 03:02:47 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
writeJSONSync(changelogIndexPath, changelogIndex, { spaces: 2 });
|
|
|
|
|
|
|
|
|
|
consola.success(
|
|
|
|
|
`${count} changelog index images have been uploaded to CDN and links have been replaced`,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-09 06:38:29 +00:00
|
|
|
// Run upload process
|
2024-12-26 03:02:47 +00:00
|
|
|
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.');
|
|
|
|
|
}
|
2026-01-27 03:51:12 +00:00
|
|
|
|
2026-04-09 06:38:29 +00:00
|
|
|
// Replace image links in posts and changelog index
|
2026-01-27 03:51:12 +00:00
|
|
|
this.replaceLinksInPosts();
|
|
|
|
|
this.replaceLinksInChangelogIndex();
|
2024-12-26 03:02:47 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-09 06:38:29 +00:00
|
|
|
// Instantiate and run
|
2024-12-26 03:02:47 +00:00
|
|
|
const instance = new ImageCDNUploader();
|
|
|
|
|
|
|
|
|
|
instance.run();
|