@@ -11,6 +11,7 @@ import {
1111 ArchiveManager ,
1212 confirmDialog ,
1313 IsNativeMobileWeb ,
14+ parseAndCreateZippableFileName ,
1415 VaultDisplayServiceInterface ,
1516} from '@standardnotes/ui-services'
1617import { Strings , StringUtils } from '@/Constants/Strings'
@@ -44,6 +45,7 @@ import { action, makeObservable, observable, reaction } from 'mobx'
4445import { AbstractViewController } from './Abstract/AbstractViewController'
4546import { NotesController } from './NotesController/NotesController'
4647import { downloadOrShareBlobBasedOnPlatform } from '@/Utils/DownloadOrShareBasedOnPlatform'
48+ import { truncateString } from '@/Components/SuperEditor/Utils'
4749
4850const UnprotectedFileActions = [ FileItemActionType . ToggleFileProtection ]
4951const NonMutatingFileActions = [ FileItemActionType . DownloadFile , FileItemActionType . PreviewFile ]
@@ -716,4 +718,86 @@ export class FilesController extends AbstractViewController<FilesControllerEvent
716718 ) ,
717719 )
718720 }
721+
722+ downloadFilesAsZip = async ( files : FileItem [ ] ) => {
723+ if ( ! this . shouldUseStreamingAPI ) {
724+ throw new Error ( 'Device does not support streaming API' )
725+ }
726+
727+ const protectedFiles = files . filter ( ( file ) => file . protected )
728+
729+ if ( protectedFiles . length > 0 ) {
730+ const authorized = await this . protections . authorizeProtectedActionForItems (
731+ protectedFiles ,
732+ ChallengeReason . AccessProtectedFile ,
733+ )
734+ if ( authorized . length === 0 ) {
735+ throw new Error ( 'Authorization is required to download protected files' )
736+ }
737+ }
738+
739+ const zipFileHandle = await window . showSaveFilePicker ( {
740+ types : [
741+ {
742+ description : 'ZIP file' ,
743+ accept : { 'application/zip' : [ '.zip' ] } ,
744+ } ,
745+ ] ,
746+ } )
747+
748+ const toast = addToast ( {
749+ type : ToastType . Progress ,
750+ title : `Downloading ${ files . length } files as archive` ,
751+ message : 'Preparing archive...' ,
752+ } )
753+
754+ try {
755+ const zip = await import ( '@zip.js/zip.js' )
756+
757+ const zipStream = await zipFileHandle . createWritable ( )
758+
759+ const zipWriter = new zip . ZipWriter ( zipStream , {
760+ level : 0 ,
761+ } )
762+
763+ const addedFilenames : string [ ] = [ ]
764+
765+ for ( const file of files ) {
766+ const fileStream = new TransformStream ( )
767+
768+ let name = parseAndCreateZippableFileName ( file . name )
769+
770+ if ( addedFilenames . includes ( name ) ) {
771+ name = `${ Date . now ( ) } ${ name } `
772+ }
773+
774+ zipWriter . add ( name , fileStream . readable ) . catch ( console . error )
775+
776+ addedFilenames . push ( name )
777+
778+ const writer = fileStream . writable . getWriter ( )
779+
780+ await this . files
781+ . downloadFile ( file , async ( bytesChunk , progress ) => {
782+ await writer . write ( bytesChunk )
783+ updateToast ( toast , {
784+ message : `Downloading file "${ truncateString ( file . name , 20 ) } "` ,
785+ progress : Math . floor ( progress . percentComplete ) ,
786+ } )
787+ } )
788+ . catch ( console . error )
789+
790+ await writer . close ( )
791+ }
792+
793+ await zipWriter . close ( )
794+ } finally {
795+ dismissToast ( toast )
796+ }
797+
798+ addToast ( {
799+ type : ToastType . Success ,
800+ message : `Successfully downloaded ${ files . length } files as archive` ,
801+ } )
802+ }
719803}
0 commit comments