@ -35,8 +35,6 @@ public enum GarbageCollectionJob: JobExecutor {
}
let timestampNow : TimeInterval = Date ( ) . timeIntervalSince1970
var attachmentLocalRelativePaths : Set < String > = [ ]
var profileAvatarFilenames : Set < String > = [ ]
GRDBStorage . shared . writeAsync (
updates : { db in
@ -203,109 +201,127 @@ public enum GarbageCollectionJob: JobExecutor {
)
" " " )
}
// / O r p h a n e d a t t a c h m e n t f i l e s - a t t a c h m e n t f i l e s w h i c h d o n ' t h a v e a n a s s o c i a t e d r e c o r d i n t h e d a t a b a s e
if details . typesToCollect . contains ( . orphanedAttachmentFiles ) {
// / * * N o t e : * * T h u m b n a i l s a r e s t o r e d i n t h e ` N S C a c h e s D i r e c t o r y ` d i r e c t o r y w h i c h s h o u l d b e a u t o m a t i c a l l y m a n a g e
// / i t ' s o w n g a r b a g e c o l l e c t i o n s o w e c a n j u s t i g n o r e i t a c c o r d i n g t o t h e v a r i o u s c o m m e n t s i n t h e f o l l o w i n g s t a c k o v e r f l o w
// / p o s t , t h e d i r e c t o r y w i l l b e c l e a r e d d u r i n g a p p u p d a t e s a s w e l l a s i f t h e s y s t e m i s r u n n i n g l o w o n m e m o r y ( i f t h e a p p i s n ' t r u n n i n g )
// / h t t p s : / / s t a c k o v e r f l o w . c o m / q u e s t i o n s / 6 8 7 9 8 6 0 / w h e n - a r e - f i l e s - f r o m - n s c a c h e s d i r e c t o r y - r e m o v e d
attachmentLocalRelativePaths = try Attachment
. select ( . localRelativeFilePath )
. filter ( Attachment . Columns . localRelativeFilePath != nil )
. asRequest ( of : String . self )
. fetchSet ( db )
}
// / O r p h a n e d p r o f i l e a v a t a r f i l e s - p r o f i l e a v a t a r f i l e s w h i c h d o n ' t h a v e a n a s s o c i a t e d r e c o r d i n t h e d a t a b a s e
if details . typesToCollect . contains ( . orphanedProfileAvatars ) {
profileAvatarFilenames = try Profile
. select ( . profilePictureFileName )
. filter ( Profile . Columns . profilePictureFileName != nil )
. asRequest ( of : String . self )
. fetchSet ( db )
}
} ,
completion : { _ , result in
// I f a n y o f t h e a b o v e f a i l e d t h e n w e d o n ' t w a n t t o c o n t i n u e ( w e w o u l d e n d u p d e l e t i n g a l l f i l e s s i n c e
// n e i t h e r o f t h e a r r a y s w o u l d h a v e b e e n p o p u l a t e d c o r r e c t l y )
guard case . success = result else {
SNLog ( " [GarbageCollectionJob] Database queries failed, skipping file cleanup " )
return
}
var deletionErrors : [ Error ] = [ ]
// O r p h a n e d a t t a c h m e n t f i l e s ( a c t u a l d e l e t i o n )
if details . typesToCollect . contains ( . orphanedAttachmentFiles ) {
// N o t e : L o o k s l i k e i n o r d e r t o r e c u r s i v e l y l o o k t h r o u g h f i l e s w e n e e d t o u s e t h e
// e n u m e r a t o r m e t h o d
let fileEnumerator = FileManager . default . enumerator (
at : URL ( fileURLWithPath : Attachment . attachmentsFolder ) ,
includingPropertiesForKeys : nil ,
options : . skipsHiddenFiles // I g n o r e t h e ` . D S _ S t o r e ` f o r t h e s i m u l a t o r
)
completion : { _ , _ in
// D i s p a t c h a s y n c s o w e c a n s w a p f r o m t h e w r i t e q u e u e t o a r e a d o n e ( w e a r e d o n e w r i t i n g )
queue . async {
// R e t r i e v e a l i s t o f a l l v a l i d a t t a c h m n e t a n d a v a t a r f i l e p a t h s
struct FileInfo {
let attachmentLocalRelativePaths : Set < String >
let profileAvatarFilenames : Set < String >
}
let allAttachmentFilePaths : Set < String > = ( fileEnumerator ?
. allObjects
. compactMap { Attachment . localRelativeFilePath ( from : ( $0 as ? URL ) ? . path ) } )
. defaulting ( to : [ ] )
. asSet ( )
let maybeFileInfo : FileInfo ? = GRDBStorage . shared . read { db -> FileInfo in
var attachmentLocalRelativePaths : Set < String > = [ ]
var profileAvatarFilenames : Set < String > = [ ]
// / O r p h a n e d a t t a c h m e n t f i l e s - a t t a c h m e n t f i l e s w h i c h d o n ' t h a v e a n a s s o c i a t e d r e c o r d i n t h e d a t a b a s e
if details . typesToCollect . contains ( . orphanedAttachmentFiles ) {
// / * * N o t e : * * T h u m b n a i l s a r e s t o r e d i n t h e ` N S C a c h e s D i r e c t o r y ` d i r e c t o r y w h i c h s h o u l d b e a u t o m a t i c a l l y m a n a g e
// / i t ' s o w n g a r b a g e c o l l e c t i o n s o w e c a n j u s t i g n o r e i t a c c o r d i n g t o t h e v a r i o u s c o m m e n t s i n t h e f o l l o w i n g s t a c k o v e r f l o w
// / p o s t , t h e d i r e c t o r y w i l l b e c l e a r e d d u r i n g a p p u p d a t e s a s w e l l a s i f t h e s y s t e m i s r u n n i n g l o w o n m e m o r y ( i f t h e a p p i s n ' t r u n n i n g )
// / h t t p s : / / s t a c k o v e r f l o w . c o m / q u e s t i o n s / 6 8 7 9 8 6 0 / w h e n - a r e - f i l e s - f r o m - n s c a c h e s d i r e c t o r y - r e m o v e d
attachmentLocalRelativePaths = try Attachment
. select ( . localRelativeFilePath )
. filter ( Attachment . Columns . localRelativeFilePath != nil )
. asRequest ( of : String . self )
. fetchSet ( db )
}
// / O r p h a n e d p r o f i l e a v a t a r f i l e s - p r o f i l e a v a t a r f i l e s w h i c h d o n ' t h a v e a n a s s o c i a t e d r e c o r d i n t h e d a t a b a s e
if details . typesToCollect . contains ( . orphanedProfileAvatars ) {
profileAvatarFilenames = try Profile
. select ( . profilePictureFileName )
. filter ( Profile . Columns . profilePictureFileName != nil )
. asRequest ( of : String . self )
. fetchSet ( db )
}
return FileInfo (
attachmentLocalRelativePaths : attachmentLocalRelativePaths ,
profileAvatarFilenames : profileAvatarFilenames
)
}
// N o t e : D i r e c t o r i e s w i l l h a v e t h e i r o w n e n t r i e s i n t h e l i s t , i f t h e r e i s a f o l d e r w i t h c o n t e n t
// t h e f i l e w i l l i n c l u d e t h e d i r e c t o r y i n i t ' s p a t h w i t h a f o r w a r d s l a s h s o w e c a n u s e t h i s t o
// d i s t i n g u i s h e m p t y d i r e c t o r i e s f r o m o n e s w i t h c o n t e n t s o w e d o n ' t u n i n t e n t i o n a l l y d e l e t e a
// d i r e c t o r y w h i c h c o n t a i n s c o n t e n t t o k e e p a s w e l l a s d e l e t e ( d i r e c t o r i e s w h i c h e n d u p e m p t y a f t e r
// t h i s c l e a n u p w i l l b e r e m o v e d d u r i n g t h e n e x t r u n )
let directoryNamesContainingContent : [ String ] = allAttachmentFilePaths
. filter { path -> Bool in path . contains ( " / " ) }
. compactMap { path -> String ? in path . components ( separatedBy : " / " ) . first }
let orphanedAttachmentFiles : Set < String > = allAttachmentFilePaths
. subtracting ( attachmentLocalRelativePaths )
. subtracting ( directoryNamesContainingContent )
// I f w e c o u l d n ' t g e t t h e f i l e l i s t s t h e n f a i l ( i n v a l i d s t a t e a n d d o n ' t w a n t t o d e l e t e a l l a t t a c h m e n t / p r o f i l e f i l e s )
guard let fileInfo : FileInfo = maybeFileInfo else {
failure ( job , StorageError . generic , false )
return
}
var deletionErrors : [ Error ] = [ ]
orphanedAttachmentFiles . forEach { filepath in
// W e d o n ' t w a n t a s i n g l e d e l e t i o n f a i l u r e t o b l o c k d e l e t i o n o f t h e o t h e r f i l e s s o t r y
// e a c h o n e a n d s t o r e t h e e r r o r t o b e u s e d t o d e t e r m i n e s u c c e s s / f a i l u r e o f t h e j o b
do {
try FileManager . default . removeItem (
atPath : URL ( fileURLWithPath : Attachment . attachmentsFolder )
. appendingPathComponent ( filepath )
. path
)
// O r p h a n e d a t t a c h m e n t f i l e s ( a c t u a l d e l e t i o n )
if details . typesToCollect . contains ( . orphanedAttachmentFiles ) {
// N o t e : L o o k s l i k e i n o r d e r t o r e c u r s i v e l y l o o k t h r o u g h f i l e s w e n e e d t o u s e t h e
// e n u m e r a t o r m e t h o d
let fileEnumerator = FileManager . default . enumerator (
at : URL ( fileURLWithPath : Attachment . attachmentsFolder ) ,
includingPropertiesForKeys : nil ,
options : . skipsHiddenFiles // I g n o r e t h e ` . D S _ S t o r e ` f o r t h e s i m u l a t o r
)
let allAttachmentFilePaths : Set < String > = ( fileEnumerator ?
. allObjects
. compactMap { Attachment . localRelativeFilePath ( from : ( $0 as ? URL ) ? . path ) } )
. defaulting ( to : [ ] )
. asSet ( )
// N o t e : D i r e c t o r i e s w i l l h a v e t h e i r o w n e n t r i e s i n t h e l i s t , i f t h e r e i s a f o l d e r w i t h c o n t e n t
// t h e f i l e w i l l i n c l u d e t h e d i r e c t o r y i n i t ' s p a t h w i t h a f o r w a r d s l a s h s o w e c a n u s e t h i s t o
// d i s t i n g u i s h e m p t y d i r e c t o r i e s f r o m o n e s w i t h c o n t e n t s o w e d o n ' t u n i n t e n t i o n a l l y d e l e t e a
// d i r e c t o r y w h i c h c o n t a i n s c o n t e n t t o k e e p a s w e l l a s d e l e t e ( d i r e c t o r i e s w h i c h e n d u p e m p t y a f t e r
// t h i s c l e a n u p w i l l b e r e m o v e d d u r i n g t h e n e x t r u n )
let directoryNamesContainingContent : [ String ] = allAttachmentFilePaths
. filter { path -> Bool in path . contains ( " / " ) }
. compactMap { path -> String ? in path . components ( separatedBy : " / " ) . first }
let orphanedAttachmentFiles : Set < String > = allAttachmentFilePaths
. subtracting ( fileInfo . attachmentLocalRelativePaths )
. subtracting ( directoryNamesContainingContent )
orphanedAttachmentFiles . forEach { filepath in
// W e d o n ' t w a n t a s i n g l e d e l e t i o n f a i l u r e t o b l o c k d e l e t i o n o f t h e o t h e r f i l e s s o t r y
// e a c h o n e a n d s t o r e t h e e r r o r t o b e u s e d t o d e t e r m i n e s u c c e s s / f a i l u r e o f t h e j o b
do {
try FileManager . default . removeItem (
atPath : URL ( fileURLWithPath : Attachment . attachmentsFolder )
. appendingPathComponent ( filepath )
. path
)
}
catch { deletionErrors . append ( error ) }
}
catch { deletionErrors . append ( error ) }
}
}
// O r p h a n e d p r o f i l e a v a t a r f i l e s ( a c t u a l d e l e t i o n )
if details . typesToCollect . contains ( . orphanedProfileAvatars ) {
let allAvatarProfileFilenames : Set < String > = ( try ? FileManager . default
. contentsOfDirectory ( atPath : ProfileManager . sharedDataProfileAvatarsDirPath ) )
. defaulting ( to : [ ] )
. asSet ( )
let orphanedAvatarFiles : Set < String > = allAvatarProfileFilenames
. subtracting ( profileAvatarFilenames )
orphanedAvatarFiles . forEach { filename in
// W e d o n ' t w a n t a s i n g l e d e l e t i o n f a i l u r e t o b l o c k d e l e t i o n o f t h e o t h e r f i l e s s o t r y
// e a c h o n e a n d s t o r e t h e e r r o r t o b e u s e d t o d e t e r m i n e s u c c e s s / f a i l u r e o f t h e j o b
do {
try FileManager . default . removeItem (
atPath : ProfileManager . profileAvatarFilepath ( filename : filename )
)
// O r p h a n e d p r o f i l e a v a t a r f i l e s ( a c t u a l d e l e t i o n )
if details . typesToCollect . contains ( . orphanedProfileAvatars ) {
let allAvatarProfileFilenames : Set < String > = ( try ? FileManager . default
. contentsOfDirectory ( atPath : ProfileManager . sharedDataProfileAvatarsDirPath ) )
. defaulting ( to : [ ] )
. asSet ( )
let orphanedAvatarFiles : Set < String > = allAvatarProfileFilenames
. subtracting ( fileInfo . profileAvatarFilenames )
orphanedAvatarFiles . forEach { filename in
// W e d o n ' t w a n t a s i n g l e d e l e t i o n f a i l u r e t o b l o c k d e l e t i o n o f t h e o t h e r f i l e s s o t r y
// e a c h o n e a n d s t o r e t h e e r r o r t o b e u s e d t o d e t e r m i n e s u c c e s s / f a i l u r e o f t h e j o b
do {
try FileManager . default . removeItem (
atPath : ProfileManager . profileAvatarFilepath ( filename : filename )
)
}
catch { deletionErrors . append ( error ) }
}
catch { deletionErrors . append ( error ) }
}
// R e p o r t a s i n g l e f i l e d e l e t i o n a s a j o b f a i l u r e ( e v e n i f o t h e r c o n t e n t w a s s u c c e s s f u l l y r e m o v e d )
guard deletionErrors . isEmpty else {
failure ( job , ( deletionErrors . first ? ? StorageError . generic ) , false )
return
}
success ( job , false )
}
// R e p o r t a s i n g l e f i l e d e l e t i o n a s a j o b f a i l u r e ( e v e n i f o t h e r c o n t e n t w a s s u c c e s s f u l l y r e m o v e d )
guard deletionErrors . isEmpty else {
failure ( job , ( deletionErrors . first ? ? StorageError . generic ) , false )
return
}
success ( job , false )
}
)
}