import SparkMD5 from 'spark-md5' import {QiniuErrorName, QiniuError, QiniuNetworkError, QiniuRequestError} from '../errors' import {Progress, LocalInfo} from '../upload' import Logger from '../logger' import {HeaderBag} from '@aws-sdk/types'; import {urlSafeBase64Decode} from './base64' export const MB = 1024 ** 2 // 文件分块 export function getChunks(file: File, blockSize: number): Blob[] { let chunkByteSize = blockSize * MB // 转换为字节 // 如果 chunkByteSize 比文件大,则直接取文件的大小 if (chunkByteSize > file.size) { chunkByteSize = file.size } else { // 因为最多 10000 chunk,所以如果 chunkSize 不符合则把每片 chunk 大小扩大两倍 while (file.size > chunkByteSize * 10000) { chunkByteSize *= 2 } } const chunks: Blob[] = [] const count = Math.ceil(file.size / chunkByteSize) for (let i = 0; i < count; i++) { const chunk = file.slice( chunkByteSize * i, i === count - 1 ? file.size : chunkByteSize * (i + 1) ) chunks.push(chunk) } return chunks } export function isMetaDataValid(params: { [key: string]: string }) { return Object.keys(params).every(key => key.indexOf('x-qn-meta-') === 0) } export function isCustomVarsValid(params: { [key: string]: string }) { return Object.keys(params).every(key => key.indexOf('x:') === 0) } export function sum(list: number[]) { return list.reduce((data, loaded) => data + loaded, 0) } export function setLocalFileInfo(localKey: string, info: LocalInfo, logger: Logger) { try { localStorage.setItem(localKey, JSON.stringify(info)) } catch (err) { logger.warn(new QiniuError( QiniuErrorName.WriteCacheFailed, `setLocalFileInfo failed: ${localKey}` )) } } export function createLocalKey(name: string, key: string | null | undefined, size: number): string { const localKey = key == null ? '_' : `_key_${key}_` return `aws_js_sdk_upload_file_name_${name}${localKey}size_${size}` } export function removeLocalFileInfo(localKey: string, logger: Logger) { try { localStorage.removeItem(localKey) } catch (err) { logger.warn(new QiniuError( QiniuErrorName.RemoveCacheFailed, `removeLocalFileInfo failed. key: ${localKey}` )) } } export function getLocalFileInfo(localKey: string, logger: Logger): LocalInfo | null { let localInfoString: string | null = null try { localInfoString = localStorage.getItem(localKey) } catch { logger.warn(new QiniuError( QiniuErrorName.ReadCacheFailed, `getLocalFileInfo failed. key: ${localKey}` )) } if (localInfoString == null) { return null } let localInfo: LocalInfo | null = null try { localInfo = JSON.parse(localInfoString) } catch { // 本地信息已被破坏,直接删除 removeLocalFileInfo(localKey, logger) logger.warn(new QiniuError( QiniuErrorName.InvalidCacheData, `getLocalFileInfo failed to parse. key: ${localKey}` )) } return localInfo } export function getAuthHeaders(token: string) { const auth = 'UpToken ' + token return {Authorization: auth} } export function getHeadersForChunkUpload(token: string) { const header = getAuthHeaders(token) return { 'content-type': 'application/octet-stream', ...header } } export function getHeadersForMkFile(token: string) { const header = getAuthHeaders(token) return { 'content-type': 'application/json', ...header } } export function createXHR(): XMLHttpRequest { // Chrome、FF if (window.XMLHttpRequest) { return new XMLHttpRequest() } // IE if (window.ActiveXObject) { return new window.ActiveXObject('Microsoft.XMLHTTP') } throw new QiniuError( QiniuErrorName.NotAvailableXMLHttpRequest, 'the current environment does not support.' ) } export async function computeMd5(data: Blob): Promise { const buffer = await readAsArrayBuffer(data) const spark = new SparkMD5.ArrayBuffer() spark.append(buffer) return spark.end() } export function readAsArrayBuffer(data: Blob): Promise { return new Promise((resolve, reject) => { const reader = new FileReader() // evt 类型目前存在问题 https://github.com/Microsoft/TypeScript/issues/4163 reader.onload = (evt: ProgressEvent) => { if (evt.target) { const body = evt.target.result resolve(body as ArrayBuffer) } else { reject(new QiniuError( QiniuErrorName.InvalidProgressEventTarget, 'progress event target is undefined' )) } } reader.onerror = () => { reject(new QiniuError( QiniuErrorName.FileReaderReadFailed, 'fileReader read failed' )) } reader.readAsArrayBuffer(data) }) } export interface ResponseSuccess { body: any, headers: any, statusCode: number } export type XHRHandler = (xhr: XMLHttpRequest) => void export interface RequestOptions { method: string onProgress?: (data: Progress) => void onCreate?: XHRHandler body?: BodyInit | null headers?: { [key: string]: string } } export type Response = Promise> export function request(url: string, options: RequestOptions): Response { return new Promise((resolve, reject) => { const xhr = createXHR() xhr.open(options.method, url) if (options.onCreate) { options.onCreate(xhr) } if (options.headers) { const headers = options.headers Object.keys(headers).forEach(k => { if (['host', 'content-length'].indexOf(k.toLowerCase()) >= 0) { return; } try { xhr.setRequestHeader(k, headers[k]) } catch (e) { // 如果有中文的话,进行encodeURIComponent。正常情况下不应该出现中文! xhr.setRequestHeader(k, encodeURIComponent(headers[k])) } }) } xhr.upload.addEventListener('progress', (evt: ProgressEvent) => { if (evt.lengthComputable && options.onProgress) { options.onProgress({ loaded: evt.loaded, total: evt.total }) } }) xhr.onreadystatechange = () => { const responseText = xhr.responseText if (xhr.readyState !== 4) { return } if (xhr.status === 0) { // 发生 0 基本都是网络错误,常见的比如跨域、断网、host 解析失败、系统拦截等等 reject(new QiniuNetworkError('network error.')) return } if (xhr.status !== 200) { let message = `xhr request failed, code: ${xhr.status}` if (responseText) { message += ` response: ${responseText}` } let data try { data = responseText } catch { // 无需处理该错误、可能拿到非 json 格式的响应是预期的 } reject(new QiniuRequestError(xhr.status, '', message, data)) return } try { const response = { body: xhr.response, headers: xhr.getAllResponseHeaders().split('\n'), status: xhr.status } const fetchHeaders = response.headers const transformedHeaders: HeaderBag = {} fetchHeaders.forEach(header => { const name = header.substr(0, header.indexOf(':') + 1); const val = header.substr(header.indexOf(':') + 1); if (name && val) { transformedHeaders[name] = val; } }) const hasReadableStream = response.body !== undefined // Return the response with buffered body if (!hasReadableStream) { response.body.text().then((body: any) => { resolve({ headers: transformedHeaders, statusCode: response.status, body, }) }) } else { resolve({ headers: transformedHeaders, statusCode: response.status, body: response.body, }) } } catch (err) { reject(err) } } xhr.send(options.body) }) } export function xmlToJson(xml: any) { // Create the return object let obj = {}; if (!xml) { return obj; } if (xml.nodeType == 1) { // element // do attributes if (xml.attributes.length > 0) { obj["@attributes"] = {}; for (let j = 0; j < xml.attributes.length; j++) { let attribute = xml.attributes.item(j); obj["@attributes"][attribute.nodeName] = attribute.nodeValue; } } } else if (xml.nodeType == 3) { // text obj = xml.nodeValue; } // do children if (xml.hasChildNodes()) { for (let i = 0, n = xml.childNodes.length; i < n; i++) { let item = xml.childNodes.item(i); let nodeName = item.nodeName; if (typeof (obj[nodeName]) == "undefined") { obj[nodeName] = xmlToJson(item); } else { if (typeof (obj[nodeName].push) == "undefined") { let old = obj[nodeName]; obj[nodeName] = []; obj[nodeName].push(old); } obj[nodeName].push(xmlToJson(item)); } } } return obj; } export function getPortFromUrl(url: string | undefined) { if (url && url.match) { let groups = url.match(/(^https?)/) if (!groups) { return '' } const type = groups[1] groups = url.match(/^https?:\/\/([^:^/]*):(\d*)/) if (groups) { return groups[2] } if (type === 'http') { return '80' } return '443' } return '' } export function getDomainFromUrl(url: string | undefined): string { if (url && url.match) { const groups = url.match(/^https?:\/\/([^:^/]*)/) return groups ? groups[1] : '' } return '' } // 非标准的 PutPolicy // 添加AWS的accessKeyId、secretAccessKey interface PutPolicy { accessKeyId: string secretAccessKey: string sessionToken: string } /** * 解析token * token --> base64(accessKeyId:secretAccessKey:sessionToken) * @param token */ export function getPutPolicy(token: string): PutPolicy { if (!token) throw new QiniuError(QiniuErrorName.InvalidToken, 'invalid token.') const decodeToken = urlSafeBase64Decode(token) const segments = decodeToken.split(':') if (segments.length < 3) throw new QiniuError(QiniuErrorName.InvalidToken, 'invalid token segments.') const accessKeyId = segments[0] if (!accessKeyId) throw new QiniuError(QiniuErrorName.InvalidToken, 'missing assess key id field.') const secretAccessKey = segments[1] if (!secretAccessKey) throw new QiniuError(QiniuErrorName.InvalidToken, 'missing secret access key field.') const sessionToken = segments[2] if (!sessionToken) throw new QiniuError(QiniuErrorName.InvalidToken, 'missing session token failed.') return {accessKeyId, secretAccessKey, sessionToken} } export function createObjectURL(file: File) { const URL = window.URL || window.webkitURL || window.mozURL return URL.createObjectURL(file) } export interface TransformValue { width: number height: number matrix: [number, number, number, number, number, number] } export function getTransform(image: HTMLImageElement, orientation: number): TransformValue { const {width, height} = image switch (orientation) { case 1: // default return { width, height, matrix: [1, 0, 0, 1, 0, 0] } case 2: // horizontal flip return { width, height, matrix: [-1, 0, 0, 1, width, 0] } case 3: // 180° rotated return { width, height, matrix: [-1, 0, 0, -1, width, height] } case 4: // vertical flip return { width, height, matrix: [1, 0, 0, -1, 0, height] } case 5: // vertical flip + -90° rotated return { width: height, height: width, matrix: [0, 1, 1, 0, 0, 0] } case 6: // -90° rotated return { width: height, height: width, matrix: [0, 1, -1, 0, height, 0] } case 7: // horizontal flip + -90° rotate return { width: height, height: width, matrix: [0, -1, -1, 0, height, width] } case 8: // 90° rotated return { width: height, height: width, matrix: [0, -1, 1, 0, 0, width] } default: throw new QiniuError( QiniuErrorName.InvalidTransformOrientation, `orientation ${orientation} is unavailable` ) } }