-
Notifications
You must be signed in to change notification settings - Fork 0
Feature/qiniu prefetch #16
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -2,6 +2,8 @@ | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import fs from 'fs' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import path from 'path' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import crypto from 'crypto' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import https from 'https' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import qiniu from 'qiniu' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { program } from 'commander' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import assert from 'assert' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -15,6 +17,9 @@ program | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .option('--qiniu-bucket <char>', '七牛的 Bucket 名称.') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .option('--qiniu-access-key <char>', '七牛 Access Key.') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .option('--qiniu-secret-key <char>', '七牛 Secret Key.') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .option('--prefetch', '上传完成后执行预取.') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .option('--prefetch-domain <char>', '预取使用的 CDN 域名, 如 https://cdn.example.com.') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .option('--prefetch-product <char>', '预取产品类型, 如 dcdn. 不指定则为融合 CDN.') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| program.parse() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -24,15 +29,28 @@ const qiniuBucket = options.qiniuBucket || process.env.QINIU_BUCKET | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const qiniuAccessKey = options.qiniuAccessKey || process.env.QINIU_ACCESS_KEY | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const qiniuSecretKey = options.qiniuSecretKey || process.env.QINIU_SECRET_KEY | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const qiniuBasePath = options.qiniuBasePath | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const prefetchEnabled = !!options.prefetch | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const prefetchDomain = options.prefetchDomain || process.env.QINIU_PREFETCH_DOMAIN | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const prefetchProduct = options.prefetchProduct | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert(distDirPath, '--upload-folder 未指定.') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert(qiniuBucket, '--qiniu-bucket 未指定.') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert(qiniuAccessKey, '--qiniu-access-key 未指定.') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert(qiniuSecretKey, '--qiniu-secret-key 未指定.') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert(qiniuBasePath, '--qiniu-base-path 未指定.') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (prefetchEnabled) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert(prefetchDomain, '--prefetch-domain 未指定, 启用 --prefetch 时必须提供.') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| distDirPath = path.resolve(process.cwd(), distDirPath) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // ============================================================ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Upload. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // ============================================================ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const uploadedKeys = [] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const walkDir = async (dirPath) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for (const filename of fs.readdirSync(dirPath)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const filePath = path.resolve(dirPath, filename) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -64,22 +82,183 @@ const walkDir = async (dirPath) => { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const uploadToken = putPolicy.uploadToken(mac) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const putExtra = new qiniu.form_up.PutExtra() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const config = new qiniu.conf.Config({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const uploadConfig = new qiniu.conf.Config({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| zone: qiniu.zone.Zone_z0 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const formUploader = new qiniu.form_up.FormUploader(config) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const uploadResult= await formUploader.putStream(uploadToken, key, stream, putExtra, () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const formUploader = new qiniu.form_up.FormUploader(uploadConfig) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const uploadResult = await formUploader.putStream(uploadToken, key, stream, putExtra, () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // ... | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const uploadedKey = uploadResult.data.key | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (uploadedKey) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.log('Upload done, key:', uploadedKey) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| uploadedKeys.push(uploadedKey) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.error('No key was returned from Qiniu, upload may failed.') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| walkDir(distDirPath) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .catch(console.error) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // ============================================================ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Prefetch. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // ============================================================ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Generate QBox access token. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * QBox signing only signs the request path, not the body. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * @param {string} apiPath - The API path, e.g. "/v2/tune/prefetch". | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * @param {string} accessKey | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * @param {string} secretKey | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * @returns {string} The full Authorization header value. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const generateQBoxToken = (apiPath, accessKey, secretKey) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const signingStr = apiPath + '\n' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const sign = crypto | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .createHmac('sha1', secretKey) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .update(signingStr) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .digest('base64') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const safeSign = sign | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .replace(/\+/g, '-') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .replace(/\//g, '_') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return `QBox ${accessKey}:${safeSign}` | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Send a prefetch request to Qiniu Fusion CDN. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * @param {string[]} urls - URLs to prefetch, max 20. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * @param {{accessKey: string, secretKey: string, product?: string}} param | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * @returns {Promise<{code: number, error: string, requestId: string, taskIds: Object<string, string>|null, invalidUrls: string[]|null, quotaDay: number, surplusDay: number}>} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const sendPrefetchRequest = (urls, { accessKey, secretKey, product }) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const apiPath = '/v2/tune/prefetch' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const token = generateQBoxToken(apiPath, accessKey, secretKey) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const payload = { urls } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (product) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| payload.product = product | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const body = JSON.stringify(payload) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return new Promise((resolve, reject) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const req = https.request({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| hostname: 'fusion.qiniuapi.com', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| port: 443, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| path: apiPath, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| method: 'POST', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| headers: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'Content-Type': 'application/json', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'Content-Length': Buffer.byteLength(body), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Authorization: token | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, (res) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let data = '' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| res.on('data', (chunk) => { data += chunk }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| res.on('end', () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| resolve(JSON.parse(data)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| reject(new Error(`Failed to parse prefetch response: ${data}`)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+155
to
+161
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let data = '' | |
| res.on('data', (chunk) => { data += chunk }) | |
| res.on('end', () => { | |
| try { | |
| resolve(JSON.parse(data)) | |
| } catch { | |
| reject(new Error(`Failed to parse prefetch response: ${data}`)) | |
| const statusCode = res.statusCode || 0 | |
| const contentType = String(res.headers['content-type'] || '').toLowerCase() | |
| const isJsonResponse = contentType.includes('application/json') | |
| let data = '' | |
| res.on('data', (chunk) => { data += chunk }) | |
| res.on('end', () => { | |
| if (!data) { | |
| resolve({ | |
| code: statusCode, | |
| error: `Prefetch API returned an empty response${statusCode ? ` (HTTP ${statusCode})` : ''}` | |
| }) | |
| return | |
| } | |
| if (!isJsonResponse) { | |
| resolve({ | |
| code: statusCode, | |
| error: `Prefetch API returned a non-JSON response${statusCode ? ` (HTTP ${statusCode})` : ''}: ${data}` | |
| }) | |
| return | |
| } | |
| try { | |
| resolve(JSON.parse(data)) | |
| } catch { | |
| resolve({ | |
| code: statusCode, | |
| error: `Failed to parse prefetch response${statusCode ? ` (HTTP ${statusCode})` : ''}: ${data}` | |
| }) |
Copilot
AI
Apr 15, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The comment says "exponential backoff", but the retry delay is currently linear (retries * 2000). Either update the delay calculation to be exponential (e.g. baseDelay * 2^(retries-1)) or adjust the comment to match the actual behavior.
| while (retries <= maxRetries && !success) { | |
| if (retries > 0) { | |
| const delay = retries * 2000 | |
| const baseDelay = 2000 | |
| while (retries <= maxRetries && !success) { | |
| if (retries > 0) { | |
| const delay = baseDelay * Math.pow(2, retries - 1) |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -1,6 +1,6 @@ | ||||||
| { | ||||||
| "name": "@lancercomet/qiniu-uploader", | ||||||
| "version": "0.1.2", | ||||||
| "version": "0.2.1-test", | ||||||
|
||||||
| "version": "0.2.1-test", | |
| "version": "0.2.1", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The README says retries use "exponential backoff", but the implementation uses a linear delay (retries * 2000). Please update either the documentation or the retry logic so they match.