diff --git a/components/object/list.vue b/components/object/list.vue index a608873..8fb391f 100644 --- a/components/object/list.vue +++ b/components/object/list.vue @@ -2,7 +2,7 @@ @@ -48,13 +48,23 @@ :bordered="false" /> @@ -72,6 +82,7 @@ diff --git a/components/replication/new-form.vue b/components/replication/new-form.vue index bdc006e..2e34096 100644 --- a/components/replication/new-form.vue +++ b/components/replication/new-form.vue @@ -1,45 +1,61 @@ diff --git a/components/tiers/change-key.vue b/components/tiers/change-key.vue index 2accc48..101a568 100644 --- a/components/tiers/change-key.vue +++ b/components/tiers/change-key.vue @@ -3,7 +3,7 @@ v-model:show="modalVisible" :mask-closable="false" preset="card" - :title="t('Update Key')" + :title="t('Update Key') + ':' + name" class="max-w-screen-md" :segmented="{ content: true, diff --git a/components/tiers/new-form.vue b/components/tiers/new-form.vue index ad023c5..fc3f30d 100644 --- a/components/tiers/new-form.vue +++ b/components/tiers/new-form.vue @@ -147,11 +147,16 @@ const handleSave = () => { storageClass: formData.value.storageclass, // 新增字段 }, }; - usetier.addTiers(data).then((res) => { - visible.value = false; - emmit("search"); - message.success(t("Create Success")); - }); + usetier + .addTiers(data) + .then((res) => { + visible.value = false; + emmit("search"); + message.success(t("Create Success")); + }) + .catch((err) => { + message.error(err.message); + }); } }); }; diff --git a/composables/useBucket.ts b/composables/useBucket.ts index a4e5ed1..25a072c 100644 --- a/composables/useBucket.ts +++ b/composables/useBucket.ts @@ -23,6 +23,9 @@ import { GetBucketLifecycleConfigurationCommand, PutBucketLifecycleConfigurationCommand, DeleteBucketLifecycleCommand, + GetBucketReplicationCommand, + PutBucketReplicationCommand, + DeleteBucketReplicationCommand, } from "@aws-sdk/client-s3"; export function useBucket({ region }: { region?: string }) { @@ -195,6 +198,52 @@ export function useBucket({ region }: { region?: string }) { return await $client.send(new DeleteBucketEncryptionCommand(params)); }; + /********************S3 Replication********************/ + const getBucketReplication = async (bucket: string) => { + const params = { + Bucket: bucket, + }; + return await $client.send(new GetBucketReplicationCommand(params)); + }; + const putBucketReplication = async (bucket: string, replication: any) => { + const params = { + Bucket: bucket, + ReplicationConfiguration: replication, + }; + return await $client.send(new PutBucketReplicationCommand(params)); + }; + const deleteBucketReplication = async (bucket: string) => { + const params = { + Bucket: bucket, + }; + return await $client.send(new DeleteBucketReplicationCommand(params)); + }; + + /********************S3 Replication********************/ + + /********************rustfs Replication target********************/ + const { $api } = useNuxtApp(); + const message = useMessage(); + const setRemoteReplicationTarget = async (bucket: string, data: any) => { + try { + return await $api.put(`/set-remote-target?bucket=${bucket}`, data); + } catch (error: any) { + message.error(error.message as string); + console.log(error); + } + // return await $api.put(`/set-remote-target?bucket=${bucket}`, data); + }; + + const listRemoteReplicationTarget = async (bucket: string) => { + return await $api.get(`/list-remote-targets?bucket=${bucket}&type=`); + }; + + const deleteRemoteReplicationTarget = async (bucket: string, arn: string) => { + return await $api.delete(`/remove-remote-target?bucket=${bucket}&arn=${arn}`); + }; + + /********************rustfs Replication target end********************/ + return { listBuckets, createBucket, @@ -218,5 +267,11 @@ export function useBucket({ region }: { region?: string }) { getBucketEncryption, putBucketEncryption, deleteBucketEncryption, + getBucketReplication, + putBucketReplication, + deleteBucketReplication, + setRemoteReplicationTarget, + listRemoteReplicationTarget, + deleteRemoteReplicationTarget, }; } diff --git a/i18n/locales/en-US.json b/i18n/locales/en-US.json index 939850b..776783b 100644 --- a/i18n/locales/en-US.json +++ b/i18n/locales/en-US.json @@ -148,6 +148,9 @@ "Clear Configuration": "Clear Configuration", "Generated Configuration": "Generated Configuration", "Upload File": "Upload File", + "Folder": "Folder", + "Update Time": "Update Time", + "Object": "Object", "Close": "Close", "Select File": "Select File", "Or": "Or", @@ -164,6 +167,7 @@ "Start Upload": "Start Upload", "Task Management": "Task Management", "Task In Progress": "Task In Progress", + "Task Completed": "Task Completed", "Upload Completed": "Upload Completed", "Browser Warning": "Browser Warning", "Cache Warning": "Cache Warning", @@ -358,6 +362,15 @@ "Expire": "Expire", "On": "On", "Off": "Off", + "Replicate existing objects": "Replicate existing objects", + "Sync metadata": "Sync metadata", + "Replicate soft delete": "Replicate soft delete", + "Replicate version delete": "Replicate version delete", + "Health Check Duration": "Health Check Duration", + "Please enter health check duration": "Please enter health check duration", + "Please enter bandwidth": "Please enter bandwidth", + "Please enter storage type": "Please enter storage type", + "Add Replication Rule": "Add Replication Rule", "No valid server configuration detected, please check server configuration": "No valid server configuration detected, please check server configuration", "Login failed, please check server configuration": "Login failed, please check server configuration", "Click to modify": "Click to modify" diff --git a/i18n/locales/zh-CN.json b/i18n/locales/zh-CN.json index 9b2fb14..599d69b 100644 --- a/i18n/locales/zh-CN.json +++ b/i18n/locales/zh-CN.json @@ -172,7 +172,10 @@ "Creation Date": "创建时间", "Bucket Not Empty": "当前桶不为空,请先删除桶内容", "Upload File": "上传文件", + "Folder": "文件夹", "Close": "关闭", + "Update Time": "更新时间", + "Object": "对象", "Select File": "选择文件", "Or": "或", "Select Folder": "选择文件夹", @@ -188,6 +191,7 @@ "Start Upload": "开始上传", "Task Management": "任务管理", "Task In Progress": "任务进行中(进行中 {uploading} 个,已成功 {completed} 个)", + "Task Completed": "任务完成", "Upload Completed": "上传完成(成功 {completed} 个,失败 {failed} 个)", "Browser Warning": "刷新/关闭浏览器 将取消当前所有任务。", "Cache Warning": "清空浏览器缓存、session过期等情况会导致任务中断或丢失,请谨慎操作。", @@ -551,5 +555,14 @@ "Off": "关闭", "No valid server configuration detected, please check server configuration": "未检测到有效服务器配置,请检查服务器配置", "Login failed, please check server configuration": "登录失败,请检查服务器配置", - "Click to modify": "点击修改" + "Click to modify": "点击修改", + "Replicate existing objects": "复制现有对象", + "Sync metadata": "同步元数据", + "Replicate soft delete": "复制软删除", + "Replicate version delete": "复制版本删除", + "Health Check Duration": "健康检查时长", + "Please enter health check duration": "请输入健康检查时长", + "Please enter bandwidth": "请输入带宽", + "Please enter storage type": "请输入存储类型", + "Add Replication Rule": "添加复制规则" } diff --git a/lib/api-client.ts b/lib/api-client.ts index 87136ff..a6ed2d4 100644 --- a/lib/api-client.ts +++ b/lib/api-client.ts @@ -30,7 +30,30 @@ class ApiClient { console.log("[request] response:", response); if (!response.ok) { - throw new Error(response.statusText); + let errorMsg = response.statusText; + try { + // 优先尝试解析为 JSON + const errorData = await response.clone().json(); + errorMsg = errorData.message || JSON.stringify(errorData) || errorMsg; + } catch (e) { + try { + // 如果不是 JSON,尝试解析为文本 + const text = await response.clone().text(); + if (text) { + // 检查是否为 XML + if (text.trim().startsWith("<")) { + // 简单提取 标签内容 + const match = text.match(/(.*?)<\/Message>/i) || text.match(/(.*?)<\/Error>/i); + errorMsg = match ? match[1] : text; + } else { + errorMsg = text; + } + } + } catch (e2) { + // 保持原有 statusText + } + } + throw new Error(errorMsg); } // 处理401 diff --git a/lib/aws4fetch.ts b/lib/aws4fetch.ts index 9461d6c..04d8eff 100644 --- a/lib/aws4fetch.ts +++ b/lib/aws4fetch.ts @@ -3,49 +3,49 @@ // use forge to create HMAC and SHA256 hashes // because crypto.subtle is not available in non-secure contexts // https://developer.mozilla.org/zh-CN/docs/Web/API/SubtleCrypto -import forge from 'node-forge' +import forge from "node-forge"; /** * @license MIT * @copyright Michael Hart 2024 */ -const encoder = new TextEncoder() +const encoder = new TextEncoder(); const HOST_SERVICES: Record = { - appstream2: 'appstream', - cloudhsmv2: 'cloudhsm', - email: 'ses', - marketplace: 'aws-marketplace', - mobile: 'AWSMobileHubService', - pinpoint: 'mobiletargeting', - queue: 'sqs', - 'git-codecommit': 'codecommit', - 'mturk-requester-sandbox': 'mturk-requester', - 'personalize-runtime': 'personalize', -} + appstream2: "appstream", + cloudhsmv2: "cloudhsm", + email: "ses", + marketplace: "aws-marketplace", + mobile: "AWSMobileHubService", + pinpoint: "mobiletargeting", + queue: "sqs", + "git-codecommit": "codecommit", + "mturk-requester-sandbox": "mturk-requester", + "personalize-runtime": "personalize", +}; // https://github.com/aws/aws-sdk-js/blob/cc29728c1c4178969ebabe3bbe6b6f3159436394/lib/signers/v4.js#L190-L198 const UNSIGNABLE_HEADERS = new Set([ - 'authorization', - 'content-type', - 'content-length', - 'user-agent', - 'presigned-expires', - 'expect', - 'x-amzn-trace-id', - 'range', - 'connection', -]) + "authorization", + "content-type", + "content-length", + "user-agent", + "presigned-expires", + "expect", + "x-amzn-trace-id", + "range", + "connection", +]); export class AwsClient { - accessKeyId: string - secretAccessKey: string - sessionToken?: string - service?: string - region?: string - cache: Map - retries: number - initRetryMs: number + accessKeyId: string; + secretAccessKey: string; + sessionToken?: string; + service?: string; + region?: string; + cache: Map; + retries: number; + initRetryMs: number; /** * @param {{ @@ -59,7 +59,16 @@ export class AwsClient { * initRetryMs?: number * }} options */ - constructor({ accessKeyId, secretAccessKey, sessionToken, service, region, cache, retries, initRetryMs }: { + constructor({ + accessKeyId, + secretAccessKey, + sessionToken, + service, + region, + cache, + retries, + initRetryMs, + }: { accessKeyId: string; secretAccessKey: string; sessionToken?: string; @@ -69,17 +78,17 @@ export class AwsClient { retries?: number; initRetryMs?: number; }) { - if (accessKeyId == null) throw new TypeError('accessKeyId is a required option') - if (secretAccessKey == null) throw new TypeError('secretAccessKey is a required option') - this.accessKeyId = accessKeyId - this.secretAccessKey = secretAccessKey - this.sessionToken = sessionToken - this.service = service - this.region = region + if (accessKeyId == null) throw new TypeError("accessKeyId is a required option"); + if (secretAccessKey == null) throw new TypeError("secretAccessKey is a required option"); + this.accessKeyId = accessKeyId; + this.secretAccessKey = secretAccessKey; + this.sessionToken = sessionToken; + this.service = service; + this.region = region; /** @type {Map} */ - this.cache = cache || new Map() - this.retries = retries != null ? retries : 10 // Up to 25.6 secs - this.initRetryMs = initRetryMs || 50 + this.cache = cache || new Map(); + this.retries = retries != null ? retries : 2; // Up to 25.6 secs + this.initRetryMs = initRetryMs || 50; } /** @@ -103,26 +112,29 @@ export class AwsClient { * @param {?AwsRequestInit} [init] * @returns {Promise} */ - async sign(input: string | Request, init: { body: ArrayBuffer | ReadableStream> | null; aws: any }) { + async sign( + input: string | Request, + init: { body: ArrayBuffer | ReadableStream> | null; aws: any } + ) { if (input instanceof Request) { - const { method, url, headers, body } = input - init = Object.assign({ method, url, headers }, init) - if (init.body == null && headers.has('Content-Type')) { - init.body = body != null && headers.has('X-Amz-Content-Sha256') ? body : await input.clone().arrayBuffer() + const { method, url, headers, body } = input; + init = Object.assign({ method, url, headers }, init); + if (init.body == null && headers.has("Content-Type")) { + init.body = body != null && headers.has("X-Amz-Content-Sha256") ? body : await input.clone().arrayBuffer(); } - input = url + input = url; } - const signer = new AwsV4Signer(Object.assign({ url: input.toString() }, init, this, init && init.aws)) - const signed = Object.assign({}, init, await signer.sign()) - delete signed.aws + const signer = new AwsV4Signer(Object.assign({ url: input.toString() }, init, this, init && init.aws)); + const signed = Object.assign({}, init, await signer.sign()); + delete signed.aws; try { - return new Request(signed.url.toString(), signed) + return new Request(signed.url.toString(), signed); } catch (e) { if (e instanceof TypeError) { // https://bugs.chromium.org/p/chromium/issues/detail?id=1360943 - return new Request(signed.url.toString(), Object.assign({ duplex: 'half' }, signed)) + return new Request(signed.url.toString(), Object.assign({ duplex: "half" }, signed)); } - throw e + throw e; } } @@ -133,40 +145,40 @@ export class AwsClient { */ async fetch(input: any, init: any) { for (let i = 0; i <= this.retries; i++) { - const fetched = fetch(await this.sign(input, init)) + const fetched = fetch(await this.sign(input, init)); if (i === this.retries) { - return fetched // No need to await if we're returning anyway + return fetched; // No need to await if we're returning anyway } - const res = await fetched + const res = await fetched; if (res.status < 500 && res.status !== 429) { - return res + return res; } - await new Promise(resolve => setTimeout(resolve, Math.random() * this.initRetryMs * Math.pow(2, i))) + await new Promise((resolve) => setTimeout(resolve, Math.random() * this.initRetryMs * Math.pow(2, i))); } - throw new Error('An unknown error occurred, ensure retries is not negative') + throw new Error("An unknown error occurred, ensure retries is not negative"); } } export class AwsV4Signer { - method: string - url: URL - headers: Headers - body: BodyInit | null | undefined - accessKeyId: string - secretAccessKey: string - sessionToken: string | undefined - service: any - region: any - cache: Map - datetime: string - signQuery: boolean | undefined - appendSessionToken: boolean - signableHeaders: any[] - signedHeaders: any - canonicalHeaders: any - credentialString: string - encodedPath: string - encodedSearch: string + method: string; + url: URL; + headers: Headers; + body: BodyInit | null | undefined; + accessKeyId: string; + secretAccessKey: string; + sessionToken: string | undefined; + service: any; + region: any; + cache: Map; + datetime: string; + signQuery: boolean | undefined; + appendSessionToken: boolean; + signableHeaders: any[]; + signedHeaders: any; + canonicalHeaders: any; + credentialString: string; + encodedPath: string; + encodedSearch: string; /** * @param {{ * method?: string @@ -186,7 +198,23 @@ export class AwsV4Signer { * singleEncode?: boolean * }} options */ - constructor({ method, url, headers, body, accessKeyId, secretAccessKey, sessionToken, service, region, cache, datetime, signQuery, appendSessionToken, allHeaders, singleEncode }: { + constructor({ + method, + url, + headers, + body, + accessKeyId, + secretAccessKey, + sessionToken, + service, + region, + cache, + datetime, + signQuery, + appendSessionToken, + allHeaders, + singleEncode, + }: { method?: string; url: string; headers?: HeadersInit; @@ -203,98 +231,101 @@ export class AwsV4Signer { allHeaders?: boolean; singleEncode?: boolean; }) { - if (url == null) throw new TypeError('url is a required option') - if (accessKeyId == null) throw new TypeError('accessKeyId is a required option') - if (secretAccessKey == null) throw new TypeError('secretAccessKey is a required option') + if (url == null) throw new TypeError("url is a required option"); + if (accessKeyId == null) throw new TypeError("accessKeyId is a required option"); + if (secretAccessKey == null) throw new TypeError("secretAccessKey is a required option"); - this.method = method || (body ? 'POST' : 'GET') - this.url = new URL(url) - this.headers = new Headers(headers || {}) - this.body = body + this.method = method || (body ? "POST" : "GET"); + this.url = new URL(url); + this.headers = new Headers(headers || {}); + this.body = body; - this.accessKeyId = accessKeyId - this.secretAccessKey = secretAccessKey - this.sessionToken = sessionToken + this.accessKeyId = accessKeyId; + this.secretAccessKey = secretAccessKey; + this.sessionToken = sessionToken; - let guessedService, guessedRegion + let guessedService, guessedRegion; if (!service || !region) { - ;[guessedService, guessedRegion] = guessServiceRegion(this.url, this.headers) + [guessedService, guessedRegion] = guessServiceRegion(this.url, this.headers); } - this.service = service || guessedService || '' - this.region = region || guessedRegion || 'us-east-1' + this.service = service || guessedService || ""; + this.region = region || guessedRegion || "us-east-1"; /** @type {Map} */ - this.cache = cache || new Map() - this.datetime = datetime || new Date().toISOString().replace(/[:-]|\.\d{3}/g, '') - this.signQuery = signQuery - this.appendSessionToken = appendSessionToken || this.service === 'iotdevicegateway' + this.cache = cache || new Map(); + this.datetime = datetime || new Date().toISOString().replace(/[:-]|\.\d{3}/g, ""); + this.signQuery = signQuery; + this.appendSessionToken = appendSessionToken || this.service === "iotdevicegateway"; - this.headers.delete('Host') // Can't be set in insecure env anyway + this.headers.delete("Host"); // Can't be set in insecure env anyway - if (this.service === 's3' && !this.signQuery && !this.headers.has('X-Amz-Content-Sha256')) { - this.headers.set('X-Amz-Content-Sha256', 'UNSIGNED-PAYLOAD') + if (this.service === "s3" && !this.signQuery && !this.headers.has("X-Amz-Content-Sha256")) { + this.headers.set("X-Amz-Content-Sha256", "UNSIGNED-PAYLOAD"); } - const params = this.signQuery ? this.url.searchParams : this.headers + const params = this.signQuery ? this.url.searchParams : this.headers; - params.set('X-Amz-Date', this.datetime) + params.set("X-Amz-Date", this.datetime); if (this.sessionToken && !this.appendSessionToken) { - params.set('X-Amz-Security-Token', this.sessionToken) + params.set("X-Amz-Security-Token", this.sessionToken); } // headers are always lowercase in keys() - this.signableHeaders = ['host', ...this.headers.keys()] - .filter(header => allHeaders || !UNSIGNABLE_HEADERS.has(header)) - .sort() + this.signableHeaders = ["host", ...this.headers.keys()] + .filter((header) => allHeaders || !UNSIGNABLE_HEADERS.has(header)) + .sort(); - this.signedHeaders = this.signableHeaders.join(';') + this.signedHeaders = this.signableHeaders.join(";"); // headers are always trimmed: // https://fetch.spec.whatwg.org/#concept-header-value-normalize this.canonicalHeaders = this.signableHeaders - .map(header => header + ':' + (header === 'host' ? this.url.host : (this.headers.get(header) || '').replace(/\s+/g, ' '))) - .join('\n') + .map( + (header) => + header + ":" + (header === "host" ? this.url.host : (this.headers.get(header) || "").replace(/\s+/g, " ")) + ) + .join("\n"); - this.credentialString = [this.datetime.slice(0, 8), this.region, this.service, 'aws4_request'].join('/') + this.credentialString = [this.datetime.slice(0, 8), this.region, this.service, "aws4_request"].join("/"); if (this.signQuery) { - if (this.service === 's3' && !params.has('X-Amz-Expires')) { - params.set('X-Amz-Expires', '86400') // 24 hours + if (this.service === "s3" && !params.has("X-Amz-Expires")) { + params.set("X-Amz-Expires", "86400"); // 24 hours } - params.set('X-Amz-Algorithm', 'AWS4-HMAC-SHA256') - params.set('X-Amz-Credential', this.accessKeyId + '/' + this.credentialString) - params.set('X-Amz-SignedHeaders', this.signedHeaders) + params.set("X-Amz-Algorithm", "AWS4-HMAC-SHA256"); + params.set("X-Amz-Credential", this.accessKeyId + "/" + this.credentialString); + params.set("X-Amz-SignedHeaders", this.signedHeaders); } - if (this.service === 's3') { + if (this.service === "s3") { try { - this.encodedPath = decodeURIComponent(this.url.pathname.replace(/\+/g, ' ')) + this.encodedPath = decodeURIComponent(this.url.pathname.replace(/\+/g, " ")); } catch (e) { - this.encodedPath = this.url.pathname + this.encodedPath = this.url.pathname; } } else { - this.encodedPath = this.url.pathname.replace(/\/+/g, '/') + this.encodedPath = this.url.pathname.replace(/\/+/g, "/"); } if (!singleEncode) { - this.encodedPath = encodeURIComponent(this.encodedPath).replace(/%2F/g, '/') + this.encodedPath = encodeURIComponent(this.encodedPath).replace(/%2F/g, "/"); } - this.encodedPath = encodeRfc3986(this.encodedPath) + this.encodedPath = encodeRfc3986(this.encodedPath); - const seenKeys = new Set() + const seenKeys = new Set(); this.encodedSearch = [...this.url.searchParams] .filter(([k]) => { - if (!k) return false // no empty keys - if (this.service === 's3') { - if (seenKeys.has(k)) return false // first val only for S3 - seenKeys.add(k) + if (!k) return false; // no empty keys + if (this.service === "s3") { + if (seenKeys.has(k)) return false; // first val only for S3 + seenKeys.add(k); } - return true + return true; }) - .map(pair => pair.map(p => encodeRfc3986(encodeURIComponent(p)))) + .map((pair) => pair.map((p) => encodeRfc3986(encodeURIComponent(p)))) // NOTE: Previously there was a TypeScript error about "k1 is possibly undefined" due to noUncheckedIndexedAccess - .sort(([k1, v1], [k2, v2]) => k1 < k2 ? -1 : k1 > k2 ? 1 : v1 < v2 ? -1 : v1 > v2 ? 1 : 0) - .map(pair => pair.join('=')) - .join('&') + .sort(([k1, v1], [k2, v2]) => (k1 < k2 ? -1 : k1 > k2 ? 1 : v1 < v2 ? -1 : v1 > v2 ? 1 : 0)) + .map((pair) => pair.join("=")) + .join("&"); } /** @@ -307,12 +338,12 @@ export class AwsV4Signer { */ async sign() { if (this.signQuery) { - this.url.searchParams.set('X-Amz-Signature', await this.signature()) + this.url.searchParams.set("X-Amz-Signature", await this.signature()); if (this.sessionToken && this.appendSessionToken) { - this.url.searchParams.set('X-Amz-Security-Token', this.sessionToken) + this.url.searchParams.set("X-Amz-Security-Token", this.sessionToken); } } else { - this.headers.set('Authorization', await this.authHeader()) + this.headers.set("Authorization", await this.authHeader()); } return { @@ -320,7 +351,7 @@ export class AwsV4Signer { url: this.url, headers: this.headers, body: this.body, - } + }; } /** @@ -328,27 +359,27 @@ export class AwsV4Signer { */ async authHeader() { return [ - 'AWS4-HMAC-SHA256 Credential=' + this.accessKeyId + '/' + this.credentialString, - 'SignedHeaders=' + this.signedHeaders, - 'Signature=' + (await this.signature()), - ].join(', ') + "AWS4-HMAC-SHA256 Credential=" + this.accessKeyId + "/" + this.credentialString, + "SignedHeaders=" + this.signedHeaders, + "Signature=" + (await this.signature()), + ].join(", "); } /** * @returns {Promise} */ async signature() { - const date = this.datetime.slice(0, 8) - const cacheKey = [this.secretAccessKey, date, this.region, this.service].join() - let kCredentials = this.cache.get(cacheKey) + const date = this.datetime.slice(0, 8); + const cacheKey = [this.secretAccessKey, date, this.region, this.service].join(); + let kCredentials = this.cache.get(cacheKey); if (!kCredentials) { - const kDate = await hmac('AWS4' + this.secretAccessKey, date) - const kRegion = await hmac(kDate, this.region) - const kService = await hmac(kRegion, this.service) - kCredentials = await hmac(kService, 'aws4_request') - this.cache.set(cacheKey, kCredentials) + const kDate = await hmac("AWS4" + this.secretAccessKey, date); + const kRegion = await hmac(kDate, this.region); + const kService = await hmac(kRegion, this.service); + kCredentials = await hmac(kService, "aws4_request"); + this.cache.set(cacheKey, kCredentials); } - return buf2hex(await hmac(kCredentials, await this.stringToSign())) + return buf2hex(await hmac(kCredentials, await this.stringToSign())); } /** @@ -356,11 +387,11 @@ export class AwsV4Signer { */ async stringToSign() { return [ - 'AWS4-HMAC-SHA256', + "AWS4-HMAC-SHA256", this.datetime, this.credentialString, buf2hex(await hash(await this.canonicalString())), - ].join('\n') + ].join("\n"); } /** @@ -371,24 +402,27 @@ export class AwsV4Signer { this.method.toUpperCase(), this.encodedPath, this.encodedSearch, - this.canonicalHeaders + '\n', + this.canonicalHeaders + "\n", this.signedHeaders, await this.hexBodyHash(), - ].join('\n') + ].join("\n"); } /** * @returns {Promise} */ async hexBodyHash() { - let hashHeader = this.headers.get('X-Amz-Content-Sha256') || (this.service === 's3' && this.signQuery ? 'UNSIGNED-PAYLOAD' : null) + let hashHeader = + this.headers.get("X-Amz-Content-Sha256") || (this.service === "s3" && this.signQuery ? "UNSIGNED-PAYLOAD" : null); if (hashHeader == null) { - if (this.body && typeof this.body !== 'string' && !('byteLength' in this.body)) { - throw new Error('body must be a string, ArrayBuffer or ArrayBufferView, unless you include the X-Amz-Content-Sha256 header') + if (this.body && typeof this.body !== "string" && !("byteLength" in this.body)) { + throw new Error( + "body must be a string, ArrayBuffer or ArrayBufferView, unless you include the X-Amz-Content-Sha256 header" + ); } - hashHeader = buf2hex(await hash(this.body || '')) + hashHeader = buf2hex(await hash(this.body || "")); } - return hashHeader + return hashHeader; } } @@ -397,31 +431,34 @@ export class AwsV4Signer { * @param {string} string * @returns {Promise} */ -async function hmac(key: string | ArrayBuffer | ArrayBufferView | forge.util.ByteStringBuffer | null, string: string | undefined) { +async function hmac( + key: string | ArrayBuffer | ArrayBufferView | forge.util.ByteStringBuffer | null, + string: string | undefined +) { const hmac = forge.hmac.create(); // Handle key conversion - if (typeof key === 'string') { - hmac.start('sha256', key); + if (typeof key === "string") { + hmac.start("sha256", key); } else if (key instanceof ArrayBuffer) { const keyArray = new Uint8Array(key); const keyString = String.fromCharCode.apply(null, [...keyArray]); - hmac.start('sha256', keyString); - } else if (key && 'getBytes' in key) { + hmac.start("sha256", keyString); + } else if (key && "getBytes" in key) { // Forge ByteStringBuffer object - hmac.start('sha256', key.getBytes()); + hmac.start("sha256", key.getBytes()); } else if (key) { // ArrayBufferView const keyArray = new Uint8Array(key.buffer); const keyString = String.fromCharCode.apply(null, [...keyArray]); - hmac.start('sha256', keyString); + hmac.start("sha256", keyString); } else { // Handle null - hmac.start('sha256', ''); + hmac.start("sha256", ""); } // Handle input string - if (typeof string === 'string') { + if (typeof string === "string") { hmac.update(string); } else { const encoded = encoder.encode(string); @@ -446,7 +483,7 @@ async function hmac(key: string | ArrayBuffer | ArrayBufferView | forge.util.Byt async function hash(content: string | ArrayBufferView | ArrayBuffer) { const md = forge.md.sha256.create(); - if (typeof content === 'string') { + if (typeof content === "string") { md.update(content); } else { // Convert BufferSource to forge format @@ -465,23 +502,23 @@ async function hash(content: string | ArrayBufferView | ArrayBu return buffer.buffer; } -const HEX_CHARS = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'] +const HEX_CHARS = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "a", "b", "c", "d", "e", "f"]; /** * @param {ArrayBufferLike} arrayBuffer * @returns {string} */ function buf2hex(arrayBuffer: ArrayBuffer) { - const buffer = new Uint8Array(arrayBuffer) - let out = '' + const buffer = new Uint8Array(arrayBuffer); + let out = ""; for (let idx = 0; idx < buffer.length; idx++) { - const n = buffer[idx] + const n = buffer[idx]; // NOTE: "n is possibly undefined" due to overzealous noUncheckedIndexedAccess - out += HEX_CHARS[(n >>> 4) & 0xF] + out += HEX_CHARS[(n >>> 4) & 0xf]; // NOTE: "n is possibly undefined" due to overzealous noUncheckedIndexedAccess - out += HEX_CHARS[n & 0xF] + out += HEX_CHARS[n & 0xf]; } - return out + return out; } /** @@ -489,7 +526,7 @@ function buf2hex(arrayBuffer: ArrayBuffer) { * @returns {string} */ function encodeRfc3986(urlEncodedStr: string) { - return urlEncodedStr.replace(/[!'()*]/g, c => '%' + c.charCodeAt(0).toString(16).toUpperCase()) + return urlEncodedStr.replace(/[!'()*]/g, (c) => "%" + c.charCodeAt(0).toString(16).toUpperCase()); } /** @@ -498,51 +535,51 @@ function encodeRfc3986(urlEncodedStr: string) { * @returns {[string, string]} [service, region] */ function guessServiceRegion(url: URL, headers: Headers) { - const { hostname, pathname } = url + const { hostname, pathname } = url; - if (hostname.endsWith('.on.aws')) { - const match = hostname.match(/^[^.]{1,63}\.lambda-url\.([^.]{1,63})\.on\.aws$/) - return match != null ? ['lambda', match[1] || ''] : ['', ''] + if (hostname.endsWith(".on.aws")) { + const match = hostname.match(/^[^.]{1,63}\.lambda-url\.([^.]{1,63})\.on\.aws$/); + return match != null ? ["lambda", match[1] || ""] : ["", ""]; } - if (hostname.endsWith('.r2.cloudflarestorage.com')) { - return ['s3', 'auto'] + if (hostname.endsWith(".r2.cloudflarestorage.com")) { + return ["s3", "auto"]; } - if (hostname.endsWith('.backblazeb2.com')) { - const match = hostname.match(/^(?:[^.]{1,63}\.)?s3\.([^.]{1,63})\.backblazeb2\.com$/) - return match != null ? ['s3', match[1] || ''] : ['', ''] + if (hostname.endsWith(".backblazeb2.com")) { + const match = hostname.match(/^(?:[^.]{1,63}\.)?s3\.([^.]{1,63})\.backblazeb2\.com$/); + return match != null ? ["s3", match[1] || ""] : ["", ""]; } - const match = hostname.replace('dualstack.', '').match(/([^.]{1,63})\.(?:([^.]{0,63})\.)?amazonaws\.com(?:\.cn)?$/) - let service = (match && match[1]) || '' - let region = match && match[2] + const match = hostname.replace("dualstack.", "").match(/([^.]{1,63})\.(?:([^.]{0,63})\.)?amazonaws\.com(?:\.cn)?$/); + let service = (match && match[1]) || ""; + let region = match && match[2]; - if (region === 'us-gov') { - region = 'us-gov-west-1' - } else if (region === 's3' || region === 's3-accelerate') { - region = 'us-east-1' - service = 's3' - } else if (service === 'iot') { - if (hostname.startsWith('iot.')) { - service = 'execute-api' - } else if (hostname.startsWith('data.jobs.iot.')) { - service = 'iot-jobs-data' + if (region === "us-gov") { + region = "us-gov-west-1"; + } else if (region === "s3" || region === "s3-accelerate") { + region = "us-east-1"; + service = "s3"; + } else if (service === "iot") { + if (hostname.startsWith("iot.")) { + service = "execute-api"; + } else if (hostname.startsWith("data.jobs.iot.")) { + service = "iot-jobs-data"; } else { - service = pathname === '/mqtt' ? 'iotdevicegateway' : 'iotdata' + service = pathname === "/mqtt" ? "iotdevicegateway" : "iotdata"; } - } else if (service === 'autoscaling') { - const targetPrefix = (headers.get('X-Amz-Target') || '').split('.')[0] - if (targetPrefix === 'AnyScaleFrontendService') { - service = 'application-autoscaling' - } else if (targetPrefix === 'AnyScaleScalingPlannerFrontendService') { - service = 'autoscaling-plans' + } else if (service === "autoscaling") { + const targetPrefix = (headers.get("X-Amz-Target") || "").split(".")[0]; + if (targetPrefix === "AnyScaleFrontendService") { + service = "application-autoscaling"; + } else if (targetPrefix === "AnyScaleScalingPlannerFrontendService") { + service = "autoscaling-plans"; } - } else if (region == null && service.startsWith('s3-')) { - region = service.slice(3).replace(/^fips-|^external-1/, '') - service = 's3' - } else if (service.endsWith('-fips')) { - service = service.slice(0, -5) + } else if (region == null && service.startsWith("s3-")) { + region = service.slice(3).replace(/^fips-|^external-1/, ""); + service = "s3"; + } else if (service.endsWith("-fips")) { + service = service.slice(0, -5); } else if (region && /-\d$/.test(service) && !/-\d$/.test(region)) { - ;[service, region] = [region, service] + [service, region] = [region, service]; } - return [HOST_SERVICES[service] || service, region || ''] + return [HOST_SERVICES[service] || service, region || ""]; } diff --git a/pages/replication/index.vue b/pages/replication/index.vue index c223029..a3273c2 100644 --- a/pages/replication/index.vue +++ b/pages/replication/index.vue @@ -2,107 +2,133 @@
-
+
- +
- {{ t('Add Replication Rule') }} + {{ t("Add Replication Rule") }} - + - {{ t('Refresh') }} + {{ t("Refresh") }}
- - + + + - - + -
diff --git a/pages/tiers/index.vue b/pages/tiers/index.vue index 53ad36f..739e458 100644 --- a/pages/tiers/index.vue +++ b/pages/tiers/index.vue @@ -2,7 +2,7 @@
@@ -18,11 +18,11 @@
- {{ t('Add Tier') }} + {{ t("Add Tier") }} - {{ t('Refresh') }} + {{ t("Refresh") }}
@@ -43,13 +43,13 @@