blobTool.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504
  1. import { GLOBAL } from '@/static/Global.js';
  2. import JsFn from '@/utils/js-fn.js';
  3. console.log(GLOBAL)
  4. const { BlobServiceClient, BlobClient } = require("@azure/storage-blob")
  5. //const s = "?sv=2019-12-12&ss=b&srt=sco&st=2020-12-22T10%3A09%3A08Z&se=2020-12-23T10%3A09%3A08Z&sp=rwdxftlacup&sig=EHyNiA5uoBPHQAHfISLei%2BWdWnldHzOYYR7ZYdtFtRo%3D"
  6. //获取文件后缀和类型
  7. function getExAndType(fileName) {
  8. let ex = fileName.substring(fileName.lastIndexOf('.') + 1)
  9. let type = 'other'
  10. ex = ex.toUpperCase()
  11. for (let key in GLOBAL.CONTENT_TYPES) {
  12. if (GLOBAL.CONTENT_TYPES[key].indexOf(ex) != -1) {
  13. type = key
  14. break
  15. }
  16. }
  17. return {
  18. ex, type
  19. }
  20. }
  21. export default class BlobTool {
  22. /**
  23. * 初始化Blob,需要先调用授权API
  24. * @param {string} blobUrl blob地址
  25. * @param {string} container 容器名称
  26. * @param {string} sasString 授权
  27. * */
  28. constructor(blobUrl, container, sasString, scope) {
  29. this.initBlob(blobUrl, container, sasString, scope)
  30. }
  31. /**
  32. * 初始化Blob,需要先调用授权API
  33. * @param {string} blobUrl blob地址
  34. * @param {string} container 容器名称
  35. * @param {string} sasString 授权
  36. * @param {string} scope 学校(school)/个人(private) 计算空间大小
  37. * */
  38. initBlob(blobUrl, container, sasString, scope) {
  39. if (blobUrl && container && sasString, scope) {
  40. //初始化containerClient
  41. this.blobService = new BlobServiceClient(blobUrl + sasString)
  42. let containerClient = this.blobService.getContainerClient(container)
  43. if (containerClient) {
  44. this.containerClient = containerClient
  45. this.container = container
  46. this.blobUrl = blobUrl
  47. this.sasString = sasString
  48. if (scope == 'private') {
  49. this.blobSpace = GLOBAL.PRIVATE_SPACE
  50. } else if (scope == 'school') {
  51. this.blobSpace = GLOBAL.SCHOOL_SPACE
  52. }
  53. } else {
  54. throw new Error("参数错误,初始化失败")
  55. }
  56. } else {
  57. throw new Error("初始化参数不完整")
  58. }
  59. }
  60. /**
  61. * 获取容器信息 (授权失败,需要账号级别的授权)
  62. * @param {object}
  63. */
  64. getProperties(options) {
  65. return new Promise((r, j) => {
  66. //const blockBlobClient = this.containerClient.getBlockBlobClient('res/基础操作范例_16x9.HTE') //blob获取成功
  67. this.containerClient.getProperties(options).then(
  68. res => {
  69. console.log('获取信息成功')
  70. console.log(res)
  71. r(res)
  72. },
  73. err => {
  74. console.log('获取信息失败')
  75. console.log(err)
  76. j(err)
  77. }
  78. )
  79. })
  80. }
  81. /**
  82. * 上传文件方法,带回调上传进度
  83. * @param {any} file 文件对象
  84. * @param {any} path 文件夹路径
  85. * @param {any} option 官方可配置项
  86. * @param {any} checkSize 上传时是否检查容器空间 官方可配置项。默认需要检查大小
  87. * @returns {object} {url, name,size,createTime,extension,type}
  88. */
  89. upload(file, path, option, checkSize = true) {
  90. return new Promise(async (r, j) => {
  91. //检查容器空间大小
  92. if (checkSize) {
  93. if (!this.totalSize) {
  94. try {
  95. this.totalSize = await this.getContainerSize()
  96. } catch (e) {
  97. this.totalSize = -1
  98. j({
  99. spaceError: '容器空间计算失败,无法上传文件'
  100. })
  101. }
  102. }
  103. if (this.totalSize == -1) {
  104. j({
  105. spaceError: 'Blob空间计算失败,无法上传文件'
  106. })
  107. } else if (this.totalSize > this.blobSpace) {
  108. j({
  109. spaceError: 'Blob空间已满,无法上传'
  110. })
  111. }
  112. }
  113. const blockBlobClient = this.containerClient.getBlockBlobClient(path + "/" + file.name)
  114. blockBlobClient.uploadBrowserData(file, option).then(
  115. res => {
  116. let url = decodeURIComponent(res._response.request.url)
  117. url = url.substring(0, url.lastIndexOf('?'))
  118. let info = getExAndType(file.name)
  119. if (this.totalSize) {
  120. this.totalSize += res._response.request.body.size
  121. }
  122. r({
  123. url: url,
  124. blob: '/' + path + "/" + file.name,
  125. name: file.name,
  126. size: res._response.request.body.size,
  127. createTime: res.lastModified.getTime(),
  128. extension: info.ex,
  129. type: info.type
  130. })
  131. },
  132. err => {
  133. j(err)
  134. }
  135. )
  136. })
  137. }
  138. /**
  139. * 处理HTEX文件类型
  140. * @param {blobList} fileList
  141. */
  142. handleHTEXFile(fileList) {
  143. let parseRes = []
  144. let names = []
  145. fileList.forEach((item, index) => {
  146. if (item.url.indexOf('/res/') > 0) {
  147. let fileItem = {}
  148. let startIndex = JsFn.findChartIndex(item.blob, '/', 1)
  149. let endIndex = JsFn.findChartIndex(item.blob, '/', 2)
  150. let name = item.blob.substring(startIndex + 1, endIndex)
  151. let nameIndex = names.indexOf(name)
  152. if (nameIndex == -1) {
  153. fileItem.url = this.blobUrl+ '/' + this.container +'/res/' + name
  154. fileItem.blob = `/res/${name}/index.json`
  155. fileItem.name = name + '.HTEX'
  156. fileItem.size = item.size
  157. fileItem.createTime = item.createTime
  158. fileItem.extension = 'HTEX'
  159. fileItem.type = 'res'
  160. names.push(name)
  161. parseRes.push(fileItem)
  162. } else {
  163. parseRes[nameIndex].size += item.size
  164. }
  165. } else {
  166. parseRes.push(item)
  167. }
  168. })
  169. return parseRes
  170. }
  171. /**
  172. * 列出所有(查询)
  173. * @param {Object} option ContainerListBlobsOptions
  174. * eg: option.prefix = 'res' 只查res文件夹下的blob
  175. * @returns {object} {blobList, continuationToken}
  176. */
  177. async listBlob(option, hendleHTEX = true) {
  178. return new Promise(async (r, j) => {
  179. let blobList = []
  180. if (this.containerClient) {
  181. let iter = this.containerClient.listBlobsFlat(option ? option : {});
  182. let blobItem = await iter.next();
  183. while (!blobItem.done) {
  184. let blobName = blobItem.value.name
  185. let info = getExAndType(blobItem.value.name)
  186. blobList.push(
  187. {
  188. url: this.blobUrl + '/' + this.container + '/' + blobName,
  189. blob: '/' + blobName,
  190. // name: blobName.substring(JsFn.findChartIndex(blobName, '/', 0) + 1),
  191. name: blobName.substring(blobName.lastIndexOf('/') + 1),
  192. size: blobItem.value.properties.contentLength,
  193. createTime: blobItem.value.properties.createdOn.getTime(),
  194. extension: info.ex,
  195. type: info.type
  196. }
  197. )
  198. blobItem = await iter.next();
  199. }
  200. if (hendleHTEX) {
  201. blobList = this.handleHTEXFile(blobList)
  202. }
  203. console.log('----', blobList)
  204. r({
  205. blobList,
  206. continuationToken: 'end'
  207. })
  208. } else {
  209. j('containerClient 错误')
  210. }
  211. })
  212. }
  213. /**
  214. * 分页列出(查询)
  215. * @param {Object} option ContainerListBlobsOptions
  216. * eg: option.prefix = 'res' 只查res文件夹下的blob
  217. * @param {Object} pageInfo
  218. * eg: pageInfo.maxPageSize 当前请求条数
  219. * eg: pageInfo.continuationToken 首次请求不需要,后面需要(首次请求会返回,下次需要传入)
  220. * @returns {object} {blobList, continuationToken}
  221. */
  222. async listBlobByPage(option, pageInfo) {
  223. return new Promise(async (r, j) => {
  224. let page = {}
  225. let blobList = []
  226. if (pageInfo && JSON.stringify(pageInfo) != '{}') {
  227. page.maxPageSize = pageInfo.maxPageSize ? pageInfo.maxPageSize : 50
  228. if (pageInfo.continuationToken) {
  229. page.continuationToken = pageInfo.continuationToken ? pageInfo.continuationToken : ''
  230. }
  231. }
  232. if (this.containerClient) {
  233. let iterator = this.containerClient.listBlobsFlat(option ? option : {}).byPage(page)
  234. let response = (await iterator.next()).value
  235. let prefixLen = response.prefix ? response.prefix.length + 1 : 0
  236. for (const blob of response.segment.blobItems) {
  237. let info = getExAndType(blob.name)
  238. blobList.push(
  239. {
  240. url: response.serviceEndpoint + response.containerName + '/' + blob.name,
  241. blob: '/' + blob.name,
  242. name: blob.name.substring(prefixLen),
  243. size: blob.properties.contentLength,
  244. createTime: blob.properties.lastModified.getTime(),
  245. extension: info.ex,
  246. type: info.type
  247. }
  248. )
  249. }
  250. let continuationToken = response.continuationToken ? response.continuationToken : 'end'
  251. r({
  252. blobList,
  253. continuationToken
  254. })
  255. } else {
  256. j('containerClient 错误')
  257. }
  258. })
  259. }
  260. /**
  261. * 删除Blob
  262. * @param {string} filePath 文件url + 容器 之后的路径
  263. */
  264. deleteBlob(filePath) {
  265. if (filePath) {
  266. filePath = filePath.substring(1)
  267. }
  268. return new Promise((r, j) => {
  269. this.containerClient.deleteBlob(filePath).then(
  270. res => {
  271. r(res)
  272. },
  273. err => {
  274. j(err)
  275. }
  276. )
  277. })
  278. }
  279. /**
  280. * 批量删除Blob 官方API授权失败(需要账号级别的授权,批量删除请访问后端API)
  281. * @param {string} files 文件url + 容器 之后的路径
  282. */
  283. deleteBlobs(files) {
  284. this.getProperties()
  285. let blobBatchClient = this.blobService.getBlobBatchClient()
  286. return new Promise((r, j) => {
  287. for (let i in files) {
  288. files[i] = files[i].substring(0, files[i].lastIndexOf('?'))
  289. }
  290. blobBatchClient.deleteBlobs(files, this.blobService.credential).then(
  291. res => {
  292. console.log('批量删除成功')
  293. },
  294. err => {
  295. console.log('批量删除失败')
  296. }
  297. )
  298. })
  299. }
  300. /**
  301. * 批量删除Blob 循环操作
  302. * @param {string} files
  303. */
  304. deleteBlobBatch(files) {
  305. return new Promise((r, j) => {
  306. let promises = []
  307. for (let item of files) {
  308. let f = item.substring(1)
  309. promises.push(this.containerClient.deleteBlob(f))
  310. }
  311. Promise.all(promises).then(
  312. res => {
  313. r(res)
  314. },
  315. err => {
  316. j(err)
  317. }
  318. )
  319. })
  320. }
  321. /**
  322. * 复制单个Blob
  323. * @param {string} targetUrl
  324. * @param {string} sourceUrl
  325. * 1、目标url(sourceUrl)不能出现中文或UTF-8码的字符, 如果有需要进行URI编码,否则会报错;
  326. * 2、目标url(sourceUrl)如果需要授权的容器,则url需要凭借授权。
  327. */
  328. copyBlob(targetUrl, sourceUrl, sas) {
  329. return new Promise((r, j) => {
  330. let newBlob = this.containerClient.getBlobClient(targetUrl)
  331. let encodeUrl = encodeURI(sourceUrl)
  332. // sas = "?sv=2020-02-10&st=2021-01-18T11%3A29%3A36Z&se=2021-01-19T11%3A59%3A36Z&sr=c&sp=rcwdl&sig=BXbsiVZ6ZogeWfyr3u2PgPVdeLlMMPOqvsh3%2BDEwnq4%3D"
  333. newBlob.beginCopyFromURL(encodeUrl + sas).then(
  334. res => {
  335. r(200)
  336. },
  337. err => {
  338. j(500)
  339. }
  340. )
  341. })
  342. }
  343. /**
  344. * 复制‘文件夹’
  345. * @param {string} targetFolder eg:'exam/评测id/paper/8b94c6b6-2572-41e5-89b9-a82fcf13891e/' 注意开头不加‘/’, 结尾需要加‘/’
  346. * @param {string} sourceFolder eg:'paper/JEFF組卷測試01'
  347. * @param {BlobTool} blobTool 非必传参数, 当目标文件和源文件不在同一个容器的时候,需要传源文件容器初始化的BlobTool
  348. */
  349. copyFolder(targetFolder, sourceFolder, blobTool) {
  350. return new Promise(async (r, j) => {
  351. let blobs = undefined
  352. let sasString = ''
  353. if (blobTool) {
  354. blobs = await blobTool.listBlob({
  355. prefix: sourceFolder + '/'
  356. }, false)
  357. sasString = blobTool.sasString
  358. } else {
  359. blobs = await this.listBlob({
  360. prefix: sourceFolder + '/'
  361. }, false)
  362. sasString = this.sasString
  363. }
  364. if (blobs && blobs.blobList) {
  365. let count = 0
  366. blobs.blobList.forEach(blobItem => {
  367. let newUrl = targetFolder + blobItem.name
  368. let newBlob = this.containerClient.getBlobClient(newUrl)
  369. let resourceUrl = encodeURI(blobItem.url)
  370. newBlob.beginCopyFromURL(resourceUrl + sasString).then().finally(() => {
  371. count++
  372. if (count == (blobs.blobList.length)) {
  373. r('复制结束后')
  374. }
  375. })
  376. })
  377. }
  378. })
  379. }
  380. /**
  381. * 判断文件是否存在
  382. * @param {string} filePath 文件路径 正确 'res/基础操作范例_16x9.HTE ' 错误 '/res/基础操作范例_16x9.HTE'
  383. * @param {object}
  384. * return true/false
  385. */
  386. exists(filePath, options) {
  387. const blockBlobClient = this.containerClient.getBlockBlobClient(filePath)
  388. return new Promise((r, j) => {
  389. blockBlobClient.exists(options).then(
  390. res => {
  391. r(res)
  392. },
  393. err => {
  394. j(err)
  395. }
  396. )
  397. })
  398. }
  399. /**
  400. * 计算容器的空间大小
  401. * return size
  402. */
  403. async getContainerSize() {
  404. return new Promise((r, j) => {
  405. this.listBlob({},false).then(
  406. res => {
  407. let totalSize = res.blobList.reduce((total, item) => {
  408. return total + parseInt(item.size)
  409. }, 0)
  410. this.totalSize = totalSize
  411. r(totalSize)
  412. },
  413. err => {
  414. j(err)
  415. }
  416. )
  417. })
  418. }
  419. /**
  420. * 判断容器的空间是否已满
  421. * @param {string} scope 校本/个人 school / private
  422. * return true/false
  423. */
  424. async isContainerFull() {
  425. return new Promise(async (r, j) => {
  426. try {
  427. let size = await this.getContainerSize()
  428. if (size > this.blobSpace) {
  429. r(true)
  430. } else {
  431. r(false)
  432. }
  433. } catch (e) {
  434. j('容器空间判断失败!')
  435. }
  436. })
  437. }
  438. /**
  439. * 计算指定文件夹的空间大小,如果不传文件夹,则查询整个容器空间
  440. * @param {string}
  441. * @param {object}
  442. * return true/false
  443. */
  444. getSize(option) {
  445. return new Promise((r, j) => {
  446. this.listBlob(option,false).then(
  447. res => {
  448. let sizeInfo = {
  449. total: 0, //所有
  450. image: 0, //内容模块图片
  451. res: 0, //内容模块教材
  452. video: 0, //内容模块视频
  453. audio: 0, //内容模块音频
  454. doc: 0, //内容模块文档
  455. other: 0, //内容模块其他文件
  456. data: 0 //除了内容模块之外的文件
  457. }
  458. let length = res.blobList.reduce((total, item) => {
  459. return total + parseInt(item.size)
  460. }, 0)
  461. sizeInfo.total = length
  462. for (let item of res.blobList) {
  463. let folder = item.blob.substring(1, JsFn.findChartIndex(item.blob, '/', 1))
  464. switch (folder) {
  465. case 'res':
  466. sizeInfo.res += item.size
  467. break
  468. case 'image':
  469. sizeInfo.image += item.size
  470. break
  471. case 'video':
  472. sizeInfo.video += item.size
  473. break
  474. case 'audio':
  475. sizeInfo.audio += item.size
  476. break
  477. case 'doc':
  478. sizeInfo.doc += item.size
  479. break
  480. case 'other':
  481. sizeInfo.other += item.size
  482. break
  483. default:
  484. sizeInfo.data += item.size
  485. break
  486. }
  487. }
  488. r(sizeInfo)
  489. },
  490. err => {
  491. j(err)
  492. }
  493. )
  494. })
  495. }
  496. }