feat: 接入腾讯云cos上传
This commit is contained in:
parent
3e3525aee1
commit
dd9000bf48
@ -28,6 +28,7 @@
|
||||
"axios": "^1.7.2",
|
||||
"chart.js": "^4.4.3",
|
||||
"compare-versions": "^6.1.1",
|
||||
"cos-js-sdk-v5": "^1.10.1",
|
||||
"dayjs": "^1.11.12",
|
||||
"echarts": "^5.5.1",
|
||||
"element-plus": "^2.7.8",
|
||||
|
||||
23
pnpm-lock.yaml
generated
23
pnpm-lock.yaml
generated
@ -47,6 +47,9 @@ importers:
|
||||
compare-versions:
|
||||
specifier: ^6.1.1
|
||||
version: 6.1.1
|
||||
cos-js-sdk-v5:
|
||||
specifier: ^1.10.1
|
||||
version: 1.10.1
|
||||
dayjs:
|
||||
specifier: ^1.11.12
|
||||
version: 1.11.13
|
||||
@ -1517,6 +1520,9 @@ packages:
|
||||
core-js@3.41.0:
|
||||
resolution: {integrity: sha512-SJ4/EHwS36QMJd6h/Rg+GyR4A5xE0FSI3eZ+iBVpfqf1x0eTSg1smWLHrA+2jQThZSh97fmSgFSU8B61nxosxA==}
|
||||
|
||||
cos-js-sdk-v5@1.10.1:
|
||||
resolution: {integrity: sha512-a4SRfCY5g6Z35C7OWe9te/S1zk77rVQzfpvZ33gmTdJQzKxbNbEG7Aw/v453XwVMsQB352FIf7KRMm5Ya/wlZQ==}
|
||||
|
||||
cose-base@1.0.3:
|
||||
resolution: {integrity: sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==}
|
||||
|
||||
@ -2098,6 +2104,10 @@ packages:
|
||||
fast-uri@3.0.6:
|
||||
resolution: {integrity: sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==}
|
||||
|
||||
fast-xml-parser@4.5.0:
|
||||
resolution: {integrity: sha512-/PlTQCI96+fZMAOLMZK4CWG1ItCbfZ/0jx7UIJFChPNrx7tcEgerUgWbeieCM9MfHInUDyK8DWYZ+YrywDJuTg==}
|
||||
hasBin: true
|
||||
|
||||
fastq@1.19.1:
|
||||
resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==}
|
||||
|
||||
@ -3347,6 +3357,9 @@ packages:
|
||||
strip-literal@2.1.1:
|
||||
resolution: {integrity: sha512-631UJ6O00eNGfMiWG78ck80dfBab8X6IVFB51jZK5Icd7XAs60Z5y7QdSd/wGIklnWvRbUNloVzhOKKmutxQ6Q==}
|
||||
|
||||
strnum@1.1.2:
|
||||
resolution: {integrity: sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA==}
|
||||
|
||||
stylis@4.3.6:
|
||||
resolution: {integrity: sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==}
|
||||
|
||||
@ -5087,6 +5100,10 @@ snapshots:
|
||||
|
||||
core-js@3.41.0: {}
|
||||
|
||||
cos-js-sdk-v5@1.10.1:
|
||||
dependencies:
|
||||
fast-xml-parser: 4.5.0
|
||||
|
||||
cose-base@1.0.3:
|
||||
dependencies:
|
||||
layout-base: 1.0.2
|
||||
@ -5806,6 +5823,10 @@ snapshots:
|
||||
|
||||
fast-uri@3.0.6: {}
|
||||
|
||||
fast-xml-parser@4.5.0:
|
||||
dependencies:
|
||||
strnum: 1.1.2
|
||||
|
||||
fastq@1.19.1:
|
||||
dependencies:
|
||||
reusify: 1.1.0
|
||||
@ -7172,6 +7193,8 @@ snapshots:
|
||||
dependencies:
|
||||
js-tokens: 9.0.1
|
||||
|
||||
strnum@1.1.2: {}
|
||||
|
||||
stylis@4.3.6: {}
|
||||
|
||||
supports-color@7.2.0:
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
allowBuilds:
|
||||
'@parcel/watcher': true
|
||||
core-js: false
|
||||
cos-js-sdk-v5: false
|
||||
es5-ext: false
|
||||
esbuild: true
|
||||
vue-demi: true
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import str from '@/utils/str'
|
||||
import { http } from './http'
|
||||
import { get } from './request'
|
||||
import { toCosUploadResponse, uploadFileToCos } from '@/utils/cosUpload'
|
||||
|
||||
export type Option = {
|
||||
value: any
|
||||
@ -199,16 +200,19 @@ const globalApi = {
|
||||
},
|
||||
|
||||
// 上传文件
|
||||
uploadFile: (data: any) =>
|
||||
http.request({
|
||||
url: '/sysFileInfo/tenUploadAll',
|
||||
method: 'post',
|
||||
timeout: 15000,
|
||||
headers: {
|
||||
'content-type': 'multipart/form-data',
|
||||
uploadFile: async (data: any) => {
|
||||
const file = data instanceof FormData ? data.get('file') : data?.file
|
||||
const fileType =
|
||||
Number(data instanceof FormData ? data.get('fileType') : data?.fileType) === 1 ? 1 : 2
|
||||
if (!(file instanceof Blob)) {
|
||||
throw new Error('请选择要上传的文件')
|
||||
}
|
||||
const result = await uploadFileToCos(file, {
|
||||
fileType,
|
||||
fileName: file instanceof File ? file.name : undefined,
|
||||
})
|
||||
return toCosUploadResponse(result)
|
||||
},
|
||||
data,
|
||||
}),
|
||||
|
||||
// 上报课堂监控截图
|
||||
reportSnapshot: (data: { sessionId: string; url: string; userId: string }) =>
|
||||
|
||||
@ -103,7 +103,11 @@ instance.interceptors.response.use(
|
||||
}
|
||||
|
||||
// 文件流不拦截
|
||||
if (response.headers['content-type']?.indexOf('octet-stream') !== -1 || code == 200)
|
||||
if (
|
||||
response.headers['content-type']?.indexOf('octet-stream') !== -1 ||
|
||||
code == 200 ||
|
||||
code == 0
|
||||
)
|
||||
return response
|
||||
|
||||
return Promise.reject(buildError(response))
|
||||
|
||||
@ -167,7 +167,7 @@ function handleDel(url: string) {
|
||||
<el-upload
|
||||
v-if="!reportFlag && canAddMore"
|
||||
class="start-btn"
|
||||
action="/api/main/sysFileInfo/tenUploadAll"
|
||||
action="#"
|
||||
:data="{ type: 1 }"
|
||||
:headers="{
|
||||
Authorization: token,
|
||||
|
||||
@ -66,7 +66,7 @@ function handleShow(url: string, isTeacher = false) {
|
||||
:style="{
|
||||
'background-image': `url(${OSS_URL}/urm/homeWork/add-image-btn.png)`,
|
||||
}"
|
||||
action="/api/main/sysFileInfo/tenUploadAll"
|
||||
action="#"
|
||||
:data="{ type: 1 }"
|
||||
:headers="{
|
||||
Authorization: token,
|
||||
@ -100,7 +100,7 @@ function handleShow(url: string, isTeacher = false) {
|
||||
<!-- 答题 -->
|
||||
<el-upload
|
||||
class="upload-demo"
|
||||
action="/api/main/sysFileInfo/tenUploadAll"
|
||||
action="#"
|
||||
:data="{ type: 1 }"
|
||||
:headers="{
|
||||
Authorization: token,
|
||||
|
||||
@ -3,8 +3,8 @@ import { storeToRefs } from 'pinia'
|
||||
import globalApi from '@/api/global'
|
||||
import { userStore } from '@/stores'
|
||||
import { $XXL, getNativeApi } from '@/utils'
|
||||
import sass from '@/utils/sass'
|
||||
import { useTips } from './useTips'
|
||||
import { uploadFileToCos, uploadNativeFilePathToCos } from '@/utils/cosUpload'
|
||||
|
||||
let monitorTimer: ReturnType<typeof setInterval> | null = null
|
||||
let monitorSessionId = ''
|
||||
@ -76,7 +76,6 @@ const parseNativeScreenshotPath = (raw: unknown): string => {
|
||||
|
||||
const uploadNativeScreenshot = async () => {
|
||||
try {
|
||||
const { token } = storeToRefs(userStore())
|
||||
const raw = $XXL.getScreenshot() || {}
|
||||
monitorLog('native getScreenshot result', raw)
|
||||
const filePath = parseNativeScreenshotPath(raw)
|
||||
@ -86,14 +85,8 @@ const uploadNativeScreenshot = async () => {
|
||||
}
|
||||
monitorLog('native screenshot path', filePath)
|
||||
|
||||
const res = await $XXL.uploadFile({
|
||||
url: `${import.meta.env.VITE_SERVER_URL}/sysFileInfo/tenUploadAll`,
|
||||
header: {
|
||||
Authorization: token.value,
|
||||
'X-Tenant-Id': sass.platform.tenantId,
|
||||
},
|
||||
filePath,
|
||||
fileKey: 'file',
|
||||
const res = await uploadNativeFilePathToCos(filePath, config => $XXL.uploadFile(config), {
|
||||
fileType: 1,
|
||||
})
|
||||
const url = normalizeUploadPath(res)
|
||||
monitorLog('native upload result', { raw: res, url })
|
||||
@ -234,9 +227,7 @@ const uploadWebScreenshot = async () => {
|
||||
return ''
|
||||
}
|
||||
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
const { data: res } = await globalApi.uploadFile(formData)
|
||||
const res = await uploadFileToCos(file, { fileType: 1 })
|
||||
const url = normalizeUploadPath(res)
|
||||
monitorLog('web upload result', { raw: res, url })
|
||||
return url
|
||||
|
||||
@ -10,6 +10,7 @@ import {
|
||||
resolveMonitorWsType,
|
||||
stopClassMonitor,
|
||||
} from './useClassMonitor'
|
||||
import { uploadNativeFilePathToCos } from '@/utils/cosUpload'
|
||||
|
||||
let SOCKET: UseWebSocketReturn<any>
|
||||
|
||||
@ -192,13 +193,8 @@ export const useSocket = () => {
|
||||
simSerialNumber: SN,
|
||||
msg: '',
|
||||
})
|
||||
const res = await $XXL.uploadFile({
|
||||
url: `${import.meta.env.VITE_SERVER_URL}/sysFileInfo/tenUploadAll`,
|
||||
header: {
|
||||
Authorization: token,
|
||||
},
|
||||
filePath,
|
||||
fileKey: 'file',
|
||||
const res = await uploadNativeFilePathToCos(filePath, config => $XXL.uploadFile(config), {
|
||||
fileType: 1,
|
||||
})
|
||||
sendMsg({
|
||||
type: 'desktopScreenshotImage',
|
||||
|
||||
@ -4,6 +4,7 @@ import { userStore } from '@/stores'
|
||||
import { XMessage } from '@/hooks'
|
||||
import dayjs from 'dayjs'
|
||||
import sass from '@/utils/sass'
|
||||
import { cosUploadRequest } from '@/utils/cosUpload'
|
||||
|
||||
const { token } = userStore()
|
||||
const OSS_URL = import.meta.env.VITE_OSS_URL
|
||||
@ -122,7 +123,7 @@ onActivated(() => {
|
||||
<div v-else class="upload-btn">
|
||||
<el-upload
|
||||
class="add-image-btn pointer"
|
||||
action="/api/main/sysFileInfo/tenUploadAll"
|
||||
action="#"
|
||||
:data="{ type: 1 }"
|
||||
:headers="{
|
||||
Authorization: token,
|
||||
@ -130,6 +131,7 @@ onActivated(() => {
|
||||
}"
|
||||
accept="image/*"
|
||||
:before-upload="handleBeforeUpload"
|
||||
:http-request="cosUploadRequest"
|
||||
:on-success="handleUploadSuccess"
|
||||
:show-file-list="false"
|
||||
:disabled="uploadLoading"
|
||||
|
||||
@ -9,7 +9,7 @@ import { useRoute, useRouter, onBeforeRouteLeave } from 'vue-router'
|
||||
|
||||
import { $XXL, getDurationText } from '@/utils'
|
||||
|
||||
import { userStore, groupPaperStore } from '@/stores'
|
||||
import { groupPaperStore } from '@/stores'
|
||||
import {
|
||||
resource_type_ai,
|
||||
resource_type_analogy,
|
||||
@ -25,8 +25,8 @@ import type { IQuestion } from '@/types/question'
|
||||
import { globalBack } from '@/router'
|
||||
import { dialog } from '@/components/mj/mj-dialog'
|
||||
import hud from '@/utils/hud'
|
||||
import sass from '@/utils/sass'
|
||||
import homeworkApi, { saveRecordAnswer, submitExam } from '@/api/subject/homework'
|
||||
import { uploadNativeFilePathToCos } from '@/utils/cosUpload'
|
||||
|
||||
const emit = defineEmits(['urgeSubmit'])
|
||||
|
||||
@ -34,7 +34,6 @@ defineOptions({
|
||||
name: 'DoWork',
|
||||
})
|
||||
|
||||
const { token } = userStore()
|
||||
const { groupPaperParams } = toRefs(groupPaperStore())
|
||||
|
||||
const resourceTypeConfig: Record<string, string> = {
|
||||
@ -247,15 +246,7 @@ async function uploadFile(options: anyObj) {
|
||||
if (options.isAndroid) {
|
||||
if (options.submitAnswerPicOriginal) {
|
||||
const res = await withUploadTimeout(
|
||||
$XXL.uploadFile({
|
||||
url: `${import.meta.env.VITE_SERVER_URL}/sysFileInfo/tenUploadAll`,
|
||||
header: {
|
||||
Authorization: token,
|
||||
'X-Tenant-Id': sass.platform.tenantId,
|
||||
},
|
||||
filePath: options.file,
|
||||
fileKey: 'file',
|
||||
}),
|
||||
uploadNativeFilePathToCos(options.file, config => $XXL.uploadFile(config)),
|
||||
)
|
||||
path = extractUploadPath(res)
|
||||
}
|
||||
@ -986,7 +977,7 @@ defineExpose({
|
||||
$filter: brightness(0) saturate(0) invert(0) sepia(0) hue-rotate(30deg);
|
||||
|
||||
.dowork {
|
||||
// background: rgb(163, 175, 199);
|
||||
background: #fff;
|
||||
position: relative;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
|
||||
@ -6,6 +6,7 @@ import { XMessage, debounce } from '@/hooks'
|
||||
import homeWorkApi from '@/api/system/homeWork'
|
||||
import LoadIcon from '@/page/system/mailbox/components/LoadIcon.vue'
|
||||
import sass from '@/utils/sass'
|
||||
import { cosUploadRequest } from '@/utils/cosUpload'
|
||||
|
||||
const router = useRouter()
|
||||
const { token } = userStore()
|
||||
@ -143,7 +144,7 @@ onMounted(async () => {
|
||||
<div>
|
||||
<el-upload
|
||||
class="pointer"
|
||||
action="/api/main/sysFileInfo/tenUploadAll"
|
||||
action="#"
|
||||
:data="{ type: 1 }"
|
||||
:headers="{
|
||||
Authorization: token,
|
||||
@ -151,6 +152,7 @@ onMounted(async () => {
|
||||
}"
|
||||
accept="image/*"
|
||||
:before-upload="handleBeforeUpload"
|
||||
:http-request="cosUploadRequest"
|
||||
:on-success="handleUploadSuccess"
|
||||
:on-error="handleUploadError"
|
||||
:show-file-list="false"
|
||||
|
||||
@ -7,10 +7,10 @@ import dayjs from 'dayjs'
|
||||
import { useRoute, useRouter, onBeforeRouteLeave } from 'vue-router'
|
||||
import { $XXL, getDurationText } from '@/utils'
|
||||
import { loading } from '@/components/mj/mj-loading'
|
||||
import { userStore } from '@/stores'
|
||||
import type { IQuestion } from '@/types/question'
|
||||
import { globalBack } from '@/router'
|
||||
import { dialog } from '@/components/mj/mj-dialog'
|
||||
import { uploadNativeFilePathToCos } from '@/utils/cosUpload'
|
||||
defineOptions({
|
||||
name: 'HomeWorkDoWork',
|
||||
})
|
||||
@ -21,8 +21,6 @@ type PaperInfo = {
|
||||
currentTitleId: string
|
||||
}
|
||||
|
||||
const { token } = userStore()
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
@ -123,14 +121,7 @@ async function uploadFile(options: anyObj) {
|
||||
let path = ''
|
||||
if (options.isAndroid) {
|
||||
if (options.submitAnswerPicOriginal) {
|
||||
const res = await $XXL.uploadFile({
|
||||
url: `${import.meta.env.VITE_SERVER_URL}/sysFileInfo/tenUploadAll`,
|
||||
header: {
|
||||
Authorization: token,
|
||||
},
|
||||
filePath: options.file,
|
||||
fileKey: 'file',
|
||||
})
|
||||
const res = await uploadNativeFilePathToCos(options.file, config => $XXL.uploadFile(config))
|
||||
path = res.url
|
||||
}
|
||||
} else {
|
||||
|
||||
@ -6,6 +6,7 @@ import { XMessage, debounce } from '@/hooks'
|
||||
import homeWorkApi from '@/api/system/homeWork'
|
||||
import LoadIcon from '@/page/system/mailbox/components/LoadIcon.vue'
|
||||
import sass from '@/utils/sass'
|
||||
import { cosUploadRequest } from '@/utils/cosUpload'
|
||||
|
||||
const router = useRouter()
|
||||
const { token } = userStore()
|
||||
@ -143,7 +144,7 @@ onMounted(async () => {
|
||||
<div>
|
||||
<el-upload
|
||||
class="pointer"
|
||||
action="/api/main/sysFileInfo/tenUploadAll"
|
||||
action="#"
|
||||
:data="{ type: 1 }"
|
||||
:headers="{
|
||||
Authorization: token,
|
||||
@ -151,6 +152,7 @@ onMounted(async () => {
|
||||
}"
|
||||
accept="image/*"
|
||||
:before-upload="handleBeforeUpload"
|
||||
:http-request="cosUploadRequest"
|
||||
:on-success="handleUploadSuccess"
|
||||
:on-error="handleUploadError"
|
||||
:show-file-list="false"
|
||||
|
||||
@ -16,6 +16,7 @@ import { compareVersions } from 'compare-versions'
|
||||
import { userStore } from '@/stores'
|
||||
import VConsole from 'vconsole'
|
||||
import { XMessage } from '@/hooks'
|
||||
import { uploadNativeFilePathToCos } from '@/utils/cosUpload'
|
||||
import hud from '@/utils/hud'
|
||||
|
||||
interface App {
|
||||
@ -169,17 +170,11 @@ export const globalStore = defineStore('global', () => {
|
||||
})
|
||||
if ((res?.appInfoList || []).length === 0) return
|
||||
const unApps = res.appInfoList
|
||||
const { token } = userStore()
|
||||
for (const i of unApps) {
|
||||
const app = apps.find((a: App) => a.pkName === i.pkName)
|
||||
const logoRes = await $XXL.uploadFile({
|
||||
url: `${import.meta.env.VITE_SERVER_URL}/sysFileInfo/tenUploadAll`,
|
||||
header: {
|
||||
Authorization: token,
|
||||
},
|
||||
filePath: app.iconPath,
|
||||
fileKey: 'file',
|
||||
})
|
||||
const logoRes = await uploadNativeFilePathToCos(app.iconPath, config =>
|
||||
$XXL.uploadFile(config),
|
||||
)
|
||||
globalApi.updateAppInfo([
|
||||
{
|
||||
pkName: i.pkName,
|
||||
|
||||
246
src/utils/cosUpload.ts
Normal file
246
src/utils/cosUpload.ts
Normal file
@ -0,0 +1,246 @@
|
||||
import COS from 'cos-js-sdk-v5'
|
||||
import { post } from '@/api/request'
|
||||
|
||||
export interface CosStsData {
|
||||
bucket: string
|
||||
durationSeconds: number
|
||||
expireAt: number
|
||||
region: string
|
||||
sessionToken: string
|
||||
tmpSecretId: string
|
||||
tmpSecretKey: string
|
||||
uploadPathPrefix: string
|
||||
}
|
||||
|
||||
export interface CosUploadResult {
|
||||
bucket: string
|
||||
filePath: string
|
||||
key: string
|
||||
region: string
|
||||
url: string
|
||||
}
|
||||
|
||||
type UploadProgress = (event: { percent: number }) => void
|
||||
type NativeUpload = (config: anyObj) => Promise<any>
|
||||
export type CosFileType = 1 | 2
|
||||
|
||||
const stsCache: Partial<Record<CosFileType, CosStsData>> = {}
|
||||
const stsPromise: Partial<Record<CosFileType, Promise<CosStsData>>> = {}
|
||||
|
||||
const normalizePrefix = (prefix = '') => prefix.replace(/^\/+/, '').replace(/\/?$/, '/')
|
||||
|
||||
const getFileExt = (fileName = '', type = '') => {
|
||||
const ext = fileName.match(/\.([a-zA-Z0-9]+)$/)?.[1]
|
||||
if (ext) return ext.toLowerCase()
|
||||
if (type.includes('/')) return type.split('/').pop() || 'png'
|
||||
return 'png'
|
||||
}
|
||||
|
||||
const getFileName = (file: File | Blob, fileName?: string) => {
|
||||
const rawName = fileName || (file instanceof File ? file.name : '')
|
||||
const ext = getFileExt(rawName, file.type)
|
||||
return `${Date.now()}-${Math.random().toString(36).slice(2, 10)}.${ext}`
|
||||
}
|
||||
|
||||
const buildKey = (sts: CosStsData, file: File | Blob, fileName?: string) => {
|
||||
const date = new Date()
|
||||
const yyyy = date.getFullYear()
|
||||
const mm = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const dd = String(date.getDate()).padStart(2, '0')
|
||||
return `${normalizePrefix(sts.uploadPathPrefix)}${yyyy}${mm}${dd}/${getFileName(file, fileName)}`
|
||||
}
|
||||
|
||||
const getCosHost = (sts: CosStsData) => `${sts.bucket}.cos.${sts.region}.myqcloud.com`
|
||||
|
||||
const normalizeCosUrl = (location: string, sts: CosStsData, key: string) => {
|
||||
const url = location || `${getCosHost(sts)}/${key}`
|
||||
return /^https?:\/\//i.test(url) ? url.replace(/^http:\/\//i, 'https://') : `https://${url}`
|
||||
}
|
||||
|
||||
const normalizeStsTime = (value: number) => (value > 9999999999 ? Math.floor(value / 1000) : value)
|
||||
|
||||
const normalizeSts = (sts: CosStsData): CosStsData => ({
|
||||
...sts,
|
||||
expireAt: normalizeStsTime(Number(sts.expireAt) || 0),
|
||||
})
|
||||
|
||||
const isValidSts = (sts: CosStsData | undefined) => {
|
||||
if (!sts) return false
|
||||
const now = Math.floor(Date.now() / 1000)
|
||||
return (
|
||||
!!sts.bucket && !!sts.region && !!sts.tmpSecretId && !!sts.tmpSecretKey && sts.expireAt > now
|
||||
)
|
||||
}
|
||||
|
||||
export const getCosSts = async (fileType: CosFileType = 2) => {
|
||||
if (isValidSts(stsCache[fileType])) return stsCache[fileType] as CosStsData
|
||||
if (!stsPromise[fileType]) {
|
||||
stsPromise[fileType] = post<CosStsData>('/common/cos/sts', { fileType }).then(res => {
|
||||
const normalized = normalizeSts(res)
|
||||
stsCache[fileType] = normalized
|
||||
return normalized
|
||||
})
|
||||
stsPromise[fileType]?.finally(() => {
|
||||
stsPromise[fileType] = undefined
|
||||
})
|
||||
}
|
||||
return stsPromise[fileType] as Promise<CosStsData>
|
||||
}
|
||||
|
||||
const createCos = (fileType: CosFileType) =>
|
||||
new COS({
|
||||
Protocol: 'https:',
|
||||
getAuthorization: async (_options, callback) => {
|
||||
const sts = await getCosSts(fileType)
|
||||
const now = Math.floor(Date.now() / 1000)
|
||||
callback({
|
||||
TmpSecretId: sts.tmpSecretId,
|
||||
TmpSecretKey: sts.tmpSecretKey,
|
||||
SecurityToken: sts.sessionToken,
|
||||
StartTime: Math.max(now - 60, sts.expireAt - sts.durationSeconds),
|
||||
ExpiredTime: sts.expireAt,
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
export const uploadFileToCos = async (
|
||||
file: File | Blob,
|
||||
options: { fileName?: string; fileType?: CosFileType; onProgress?: UploadProgress } = {},
|
||||
): Promise<CosUploadResult> => {
|
||||
const fileType = options.fileType || 2
|
||||
const sts = await getCosSts(fileType)
|
||||
const key = buildKey(sts, file, options.fileName)
|
||||
const result = await createCos(fileType).putObject({
|
||||
Bucket: sts.bucket,
|
||||
Region: sts.region,
|
||||
Key: key,
|
||||
Body: file,
|
||||
ContentType: file.type || undefined,
|
||||
onProgress: progress => {
|
||||
options.onProgress?.({ percent: Math.round((progress.percent || 0) * 100) })
|
||||
},
|
||||
})
|
||||
const url = normalizeCosUrl(result.Location, sts, key)
|
||||
return {
|
||||
bucket: sts.bucket,
|
||||
filePath: url,
|
||||
key,
|
||||
region: sts.region,
|
||||
url,
|
||||
}
|
||||
}
|
||||
|
||||
const toHex = (buffer: ArrayBuffer) =>
|
||||
Array.from(new Uint8Array(buffer))
|
||||
.map(byte => byte.toString(16).padStart(2, '0'))
|
||||
.join('')
|
||||
|
||||
const sha1 = async (value: string) => {
|
||||
const data = new TextEncoder().encode(value)
|
||||
return toHex(await crypto.subtle.digest('SHA-1', data))
|
||||
}
|
||||
|
||||
const hmacSha1 = async (key: string, value: string) => {
|
||||
const cryptoKey = await crypto.subtle.importKey(
|
||||
'raw',
|
||||
new TextEncoder().encode(key),
|
||||
{ name: 'HMAC', hash: 'SHA-1' },
|
||||
false,
|
||||
['sign'],
|
||||
)
|
||||
return toHex(await crypto.subtle.sign('HMAC', cryptoKey, new TextEncoder().encode(value)))
|
||||
}
|
||||
|
||||
const encodeBase64 = (value: string) => {
|
||||
const bytes = new TextEncoder().encode(value)
|
||||
let binary = ''
|
||||
bytes.forEach(byte => {
|
||||
binary += String.fromCharCode(byte)
|
||||
})
|
||||
return btoa(binary)
|
||||
}
|
||||
|
||||
const createPostObjectFields = async (sts: CosStsData, key: string) => {
|
||||
const now = Math.floor(Date.now() / 1000)
|
||||
const start = Math.max(now - 60, sts.expireAt - sts.durationSeconds)
|
||||
const keyTime = `${start};${sts.expireAt}`
|
||||
const policy = encodeBase64(
|
||||
JSON.stringify({
|
||||
expiration: new Date(sts.expireAt * 1000).toISOString(),
|
||||
conditions: [
|
||||
{ bucket: sts.bucket },
|
||||
{ key },
|
||||
{ 'q-sign-algorithm': 'sha1' },
|
||||
{ 'q-ak': sts.tmpSecretId },
|
||||
{ 'q-sign-time': keyTime },
|
||||
{ 'x-cos-security-token': sts.sessionToken },
|
||||
],
|
||||
}),
|
||||
)
|
||||
const signKey = await hmacSha1(sts.tmpSecretKey, keyTime)
|
||||
const signature = await hmacSha1(signKey, await sha1(policy))
|
||||
return {
|
||||
key,
|
||||
policy,
|
||||
'q-sign-algorithm': 'sha1',
|
||||
'q-ak': sts.tmpSecretId,
|
||||
'q-key-time': keyTime,
|
||||
'q-signature': signature,
|
||||
'x-cos-security-token': sts.sessionToken,
|
||||
}
|
||||
}
|
||||
|
||||
export const uploadNativeFilePathToCos = async (
|
||||
filePath: string,
|
||||
nativeUpload: NativeUpload,
|
||||
options: { fileName?: string; fileType?: CosFileType; mimeType?: string } = {},
|
||||
): Promise<CosUploadResult> => {
|
||||
const sts = await getCosSts(options.fileType || 2)
|
||||
const fileName = options.fileName || filePath.split(/[\\/]/).pop() || ''
|
||||
const key = buildKey(sts, new Blob([], { type: options.mimeType || 'image/png' }), fileName)
|
||||
const formData = await createPostObjectFields(sts, key)
|
||||
await nativeUpload({
|
||||
url: `https://${getCosHost(sts)}`,
|
||||
header: {},
|
||||
data: formData,
|
||||
filePath,
|
||||
fileKey: 'file',
|
||||
formData,
|
||||
})
|
||||
const url = normalizeCosUrl('', sts, key)
|
||||
return {
|
||||
bucket: sts.bucket,
|
||||
filePath: url,
|
||||
key,
|
||||
region: sts.region,
|
||||
url,
|
||||
}
|
||||
}
|
||||
|
||||
export const toCosUploadResponse = (result: CosUploadResult) => ({
|
||||
data: {
|
||||
code: 200,
|
||||
data: result,
|
||||
message: '',
|
||||
},
|
||||
})
|
||||
|
||||
export const cosUploadRequest = async (options: anyObj) => {
|
||||
try {
|
||||
const result = await uploadFileToCos(options.file, {
|
||||
fileType: options.data?.fileType || 2,
|
||||
fileName: options.file?.name,
|
||||
onProgress: event => options.onProgress?.(event),
|
||||
})
|
||||
const response = {
|
||||
code: 200,
|
||||
data: result,
|
||||
message: '',
|
||||
}
|
||||
options.onSuccess?.(response)
|
||||
return response
|
||||
} catch (error) {
|
||||
options.onError?.(error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user