feat(edit): 编辑页上报ozon 开发
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
import { createHash, createHmac } from 'node:crypto';
|
||||
import {
|
||||
GetObjectCommand,
|
||||
PutObjectCommand,
|
||||
type S3Client,
|
||||
} from '@aws-sdk/client-s3';
|
||||
import sharp from 'sharp';
|
||||
import type { OzonPublishItemSnapshot } from '@atlas/contracts';
|
||||
|
||||
export type PublishMediaConfig = {
|
||||
provider: string;
|
||||
bucket: string;
|
||||
downloadOrigin: string;
|
||||
accessKey: string;
|
||||
secretKey: string;
|
||||
ttlSeconds: number;
|
||||
};
|
||||
|
||||
/** 七牛私有下载凭证签署包含过期时间的完整 URL,凭证只在提交期间保留。 */
|
||||
export function qiniuPrivateDownloadUrl(
|
||||
origin: string,
|
||||
key: string,
|
||||
deadline: number,
|
||||
accessKey: string,
|
||||
secretKey: string,
|
||||
): string {
|
||||
const base = new URL(origin);
|
||||
if (
|
||||
base.protocol !== 'https:' ||
|
||||
base.username ||
|
||||
base.password ||
|
||||
base.search ||
|
||||
base.hash ||
|
||||
base.pathname !== '/' ||
|
||||
!accessKey ||
|
||||
!secretKey ||
|
||||
!Number.isSafeInteger(deadline) ||
|
||||
deadline <= 0
|
||||
)
|
||||
throw new Error('请配置七牛公网 HTTPS 下载域名及签名凭据');
|
||||
const url = `${base.origin}/${key.split('/').map(encodeURIComponent).join('/')}?e=${deadline}`;
|
||||
const signature = createHmac('sha1', secretKey)
|
||||
.update(url)
|
||||
.digest('base64')
|
||||
.replace(/\+/g, '-')
|
||||
.replace(/\//g, '_');
|
||||
return `${url}&token=${accessKey}:${signature}`;
|
||||
}
|
||||
|
||||
/** 为本次发布复制不可变图片,源 checksum 不符时禁止交付;WebP 转为 JPEG。 */
|
||||
export async function prepareOzonImages(
|
||||
storage: S3Client,
|
||||
snapshot: OzonPublishItemSnapshot,
|
||||
workspaceId: string,
|
||||
jobId: string,
|
||||
config: PublishMediaConfig,
|
||||
): Promise<string[]> {
|
||||
if (
|
||||
config.provider !== 'qiniu_kodo' ||
|
||||
!config.downloadOrigin ||
|
||||
!Number.isSafeInteger(config.ttlSeconds) ||
|
||||
config.ttlSeconds < 3600
|
||||
)
|
||||
throw new Error(
|
||||
'上架图片交付尚未配置,请配置七牛 HTTPS 下载域名和有效期',
|
||||
);
|
||||
const urls: string[] = [];
|
||||
for (const media of snapshot.media) {
|
||||
if (
|
||||
media.storage_provider !== config.provider ||
|
||||
media.bucket !== config.bucket
|
||||
)
|
||||
throw new Error('图片存储环境与当前七牛配置不一致,请先迁移素材');
|
||||
const object = await storage.send(
|
||||
new GetObjectCommand({
|
||||
Bucket: media.bucket,
|
||||
Key: media.object_key,
|
||||
}),
|
||||
{ abortSignal: AbortSignal.timeout(60_000) },
|
||||
);
|
||||
if (!object.Body) throw new Error('发布源图片不存在');
|
||||
const chunks: Buffer[] = [];
|
||||
let size = 0;
|
||||
for await (const chunk of object.Body as AsyncIterable<Uint8Array>) {
|
||||
size += chunk.byteLength;
|
||||
if (size > 20 * 1024 * 1024) throw new Error('发布图片超过 20 MB');
|
||||
chunks.push(Buffer.from(chunk));
|
||||
}
|
||||
const source = Buffer.concat(chunks);
|
||||
if (
|
||||
createHash('sha256').update(source).digest('hex') !==
|
||||
media.checksum_sha256
|
||||
)
|
||||
throw new Error('图片内容已变更,请重新保存草稿后上架');
|
||||
const image = sharp(source, {
|
||||
limitInputPixels: 100_000_000,
|
||||
failOn: 'error',
|
||||
});
|
||||
const meta = await image.metadata();
|
||||
if (!['jpeg', 'png', 'webp'].includes(meta.format ?? ''))
|
||||
throw new Error('发布图片实际格式不受支持');
|
||||
const webp = meta.format === 'webp';
|
||||
const bytes = webp
|
||||
? await image
|
||||
.rotate()
|
||||
.flatten({ background: '#ffffff' })
|
||||
.jpeg({ quality: 95 })
|
||||
.toBuffer()
|
||||
: source;
|
||||
const ext = meta.format === 'png' ? 'png' : 'jpg';
|
||||
const key = `publishing/${workspaceId}/${jobId}/${media.asset_id}-${media.checksum_sha256}.${ext}`;
|
||||
await storage.send(
|
||||
new PutObjectCommand({
|
||||
Bucket: media.bucket,
|
||||
Key: key,
|
||||
Body: bytes,
|
||||
ContentType: ext === 'png' ? 'image/png' : 'image/jpeg',
|
||||
}),
|
||||
{ abortSignal: AbortSignal.timeout(60_000) },
|
||||
);
|
||||
urls.push(
|
||||
qiniuPrivateDownloadUrl(
|
||||
config.downloadOrigin,
|
||||
key,
|
||||
Math.floor(Date.now() / 1000) + config.ttlSeconds,
|
||||
config.accessKey,
|
||||
config.secretKey,
|
||||
),
|
||||
);
|
||||
}
|
||||
return urls;
|
||||
}
|
||||
Reference in New Issue
Block a user