1/* 2 * Copyright (C) 2023 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16import JSZip from 'jszip'; 17import {ArrayUtils} from './array_utils'; 18import {FunctionUtils, OnProgressUpdateType} from './function_utils'; 19 20export type OnFile = (file: File, parentArchive: File | undefined) => void; 21 22export class FileUtils { 23 //allow: letters/numbers/underscores with delimiters . - # (except at start and end) 24 static readonly DOWNLOAD_FILENAME_REGEX = /^\w+?((|#|-|\.)\w+)+$/; 25 static readonly ILLEGAL_FILENAME_CHARACTERS_REGEX = /[^A-Za-z0-9-#._]/g; 26 27 static getFileExtension(filename: string): string | undefined { 28 const lastDot = filename.lastIndexOf('.'); 29 if (lastDot === -1) { 30 return undefined; 31 } 32 return filename.slice(lastDot + 1); 33 } 34 35 static removeDirFromFileName(name: string): string { 36 if (name.includes('/')) { 37 const startIndex = name.lastIndexOf('/') + 1; 38 return name.slice(startIndex); 39 } else { 40 return name; 41 } 42 } 43 44 static removeExtensionFromFilename(name: string): string { 45 if (name.includes('.')) { 46 const lastIndex = name.lastIndexOf('.'); 47 return name.slice(0, lastIndex); 48 } else { 49 return name; 50 } 51 } 52 53 static async createZipArchive( 54 files: File[], 55 progressCallback?: OnProgressUpdateType, 56 ): Promise<Blob> { 57 const zip = new JSZip(); 58 for (let i = 0; i < files.length; i++) { 59 const file = files[i]; 60 const blob = await file.arrayBuffer(); 61 zip.file(file.name, blob); 62 if (progressCallback) progressCallback((i + 1) / files.length); 63 } 64 return await zip.generateAsync({type: 'blob'}); 65 } 66 67 static async unzipFile( 68 file: Blob, 69 onProgressUpdate: OnProgressUpdateType = FunctionUtils.DO_NOTHING, 70 ): Promise<File[]> { 71 const unzippedFiles: File[] = []; 72 const zip = new JSZip(); 73 const content = await zip.loadAsync(file); 74 75 const filenames = Object.keys(content.files); 76 for (const [index, filename] of filenames.entries()) { 77 const file = content.files[filename]; 78 if (file.dir) { 79 // Ignore directories 80 continue; 81 } else { 82 const fileBlob = await file.async('blob'); 83 const unzippedFile = new File([fileBlob], filename); 84 if (await FileUtils.isZipFile(unzippedFile)) { 85 unzippedFiles.push(...(await FileUtils.unzipFile(fileBlob))); 86 } else { 87 unzippedFiles.push(unzippedFile); 88 } 89 } 90 91 onProgressUpdate((100 * (index + 1)) / filenames.length); 92 } 93 94 return unzippedFiles; 95 } 96 97 static async decompressGZipFile(file: File): Promise<File> { 98 const decompressionStream = new (window as any).DecompressionStream('gzip'); 99 const decompressedStream = file.stream().pipeThrough(decompressionStream); 100 const fileBlob = await new Response(decompressedStream).blob(); 101 return new File( 102 [fileBlob], 103 FileUtils.removeExtensionFromFilename(file.name), 104 ); 105 } 106 107 static async isZipFile(file: File): Promise<boolean> { 108 return FileUtils.isMatchForMagicNumber(file, FileUtils.PK_ZIP_MAGIC_NUMBER); 109 } 110 111 static async isGZipFile(file: File): Promise<boolean> { 112 return FileUtils.isMatchForMagicNumber(file, FileUtils.GZIP_MAGIC_NUMBER); 113 } 114 115 private static async isMatchForMagicNumber( 116 file: File, 117 magicNumber: number[], 118 ): Promise<boolean> { 119 const bufferStart = new Uint8Array((await file.arrayBuffer()).slice(0, 2)); 120 return ArrayUtils.equal(bufferStart, magicNumber); 121 } 122 123 private static readonly GZIP_MAGIC_NUMBER = [0x1f, 0x8b]; 124 private static readonly PK_ZIP_MAGIC_NUMBER = [0x50, 0x4b]; 125} 126