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(files: File[]): Promise<Blob> { 54 const zip = new JSZip(); 55 for (let i = 0; i < files.length; i++) { 56 const file = files[i]; 57 const blob = await file.arrayBuffer(); 58 zip.file(file.name, blob); 59 } 60 return await zip.generateAsync({type: 'blob'}); 61 } 62 63 static async unzipFile( 64 file: Blob, 65 onProgressUpdate: OnProgressUpdateType = FunctionUtils.DO_NOTHING, 66 ): Promise<File[]> { 67 const unzippedFiles: File[] = []; 68 const zip = new JSZip(); 69 const content = await zip.loadAsync(file); 70 71 const filenames = Object.keys(content.files); 72 for (const [index, filename] of filenames.entries()) { 73 const file = content.files[filename]; 74 if (file.dir) { 75 // Ignore directories 76 continue; 77 } else { 78 const fileBlob = await file.async('blob'); 79 const unzippedFile = new File([fileBlob], filename); 80 unzippedFiles.push(unzippedFile); 81 } 82 83 onProgressUpdate((100 * (index + 1)) / filenames.length); 84 } 85 86 return unzippedFiles; 87 } 88 89 static async decompressGZipFile(file: File): Promise<File> { 90 const decompressionStream = new (window as any).DecompressionStream('gzip'); 91 const decompressedStream = file.stream().pipeThrough(decompressionStream); 92 const fileBlob = await new Response(decompressedStream).blob(); 93 return new File( 94 [fileBlob], 95 FileUtils.removeExtensionFromFilename(file.name), 96 ); 97 } 98 99 static async isZipFile(file: File): Promise<boolean> { 100 return FileUtils.isMatchForMagicNumber(file, FileUtils.PK_ZIP_MAGIC_NUMBER); 101 } 102 103 static async isGZipFile(file: File): Promise<boolean> { 104 return FileUtils.isMatchForMagicNumber(file, FileUtils.GZIP_MAGIC_NUMBER); 105 } 106 107 private static async isMatchForMagicNumber( 108 file: File, 109 magicNumber: number[], 110 ): Promise<boolean> { 111 const bufferStart = new Uint8Array((await file.arrayBuffer()).slice(0, 2)); 112 return ArrayUtils.equal(bufferStart, magicNumber); 113 } 114 115 private static readonly GZIP_MAGIC_NUMBER = [0x1f, 0x8b]; 116 private static readonly PK_ZIP_MAGIC_NUMBER = [0x50, 0x4b]; 117} 118