@ -6,6 +6,15 @@ import GRDB
import SessionSnodeKit
import SessionUtilitiesKit
// MARK: - S i n g l e t o n
public extension Singleton {
static let displayPictureManager : SingletonConfig < DisplayPictureManager > = Dependencies . create (
identifier : " displayPictureManager " ,
createInstance : { dependencies in DisplayPictureManager ( using : dependencies ) }
)
}
// MARK: - L o g . C a t e g o r y
public extension Log . Category {
@ -13,7 +22,10 @@ public extension Log.Category {
}
// MARK: - D i s p l a y P i c t u r e M a n a g e r
public struct DisplayPictureManager {
public class DisplayPictureManager {
public typealias UploadResult = ( downloadUrl : String , fileName : String , encryptionKey : Data )
public enum Update {
case none
@ -35,8 +47,19 @@ public struct DisplayPictureManager {
internal static let nonceLength : Int = 12
internal static let tagLength : Int = 16
private static var scheduleDownloadsPublisher : AnyPublisher < Void , Never > ?
private static let scheduleDownloadsTrigger : PassthroughSubject < ( ) , Never > = PassthroughSubject ( )
private let dependencies : Dependencies
private let scheduleDownloads : PassthroughSubject < ( ) , Never > = PassthroughSubject ( )
private var scheduleDownloadsCancellable : AnyCancellable ?
// MARK: - I n i t a l i z a t i o n
init ( using dependencies : Dependencies ) {
self . dependencies = dependencies
setupThrottledDownloading ( )
}
// MARK: - G e n e r a l
public static func isTooLong ( profileUrl : String ) -> Bool {
// / S t r i n g . u t f 8 C S t r i n g w i l l i n c l u d e t h e n u l l t e r m i n a t o r ( I n t 8 ) 0 a s t h e e n d o f s t r i n g b u f f e r .
@ -46,7 +69,7 @@ public struct DisplayPictureManager {
return ( profileUrl . utf8CString . count > LibSession . sizeMaxProfileUrlBytes )
}
public static func sharedDataDisplayPictureDirPath ( using dependencies : Dependencies ) -> String {
public func sharedDataDisplayPictureDirPath ( ) -> String {
let path : String = URL ( fileURLWithPath : dependencies [ singleton : . fileManager ] . appSharedDataDirectoryPath )
. appendingPathComponent ( " ProfileAvatars " ) // s t r i n g l i n t : i g n o r e
. path
@ -57,11 +80,7 @@ public struct DisplayPictureManager {
// MARK: - L o a d i n g
public static func displayPicture (
_ db : Database ,
id : OwnerId ,
using dependencies : Dependencies
) -> Data ? {
public func displayPicture ( _ db : Database , id : OwnerId ) -> Data ? {
let maybeOwner : Owner ? = {
switch id {
case . user ( let id ) : return try ? Profile . fetchOne ( db , id : id ) . map { Owner . user ( $0 ) }
@ -72,41 +91,34 @@ public struct DisplayPictureManager {
guard let owner : Owner = maybeOwner else { return nil }
return displayPicture ( owner : owner , using : dependencies )
return displayPicture ( owner : owner )
}
@ discardableResult public static func displayPicture (
owner : Owner ,
using dependencies : Dependencies
) -> Data ? {
@ discardableResult public func displayPicture ( owner : Owner ) -> Data ? {
switch ( owner . fileName , owner . canDownloadImage ) {
case ( . some ( let fileName ) , _ ) :
return loadDisplayPicture ( for : fileName , owner : owner , using : dependencies )
return loadDisplayPicture ( for : fileName , owner : owner )
case ( _ , true ) :
scheduleDownload ( for : owner , currentFileInvalid : false , using : dependencies )
scheduleDownload ( for : owner , currentFileInvalid : false )
return nil
default : return nil
}
}
private static func loadDisplayPicture (
for fileName : String ,
owner : Owner ,
using dependencies : Dependencies
) -> Data ? {
private func loadDisplayPicture ( for fileName : String , owner : Owner ) -> Data ? {
if let cachedImageData : Data = dependencies [ cache : . displayPicture ] . imageData [ fileName ] {
return cachedImageData
}
guard
! fileName . isEmpty ,
let data : Data = loadDisplayPictureFromDisk ( for : fileName , using : dependencies ),
let data : Data = loadDisplayPictureFromDisk ( for : fileName ),
data . isValidImage
else {
// I f w e c a n ' t l o a d t h e a v a t a r o r i t ' s a n i n v a l i d / c o r r u p t e d i m a g e t h e n c l e a r i t o u t a n d r e - d o w n l o a d
scheduleDownload ( for : owner , currentFileInvalid : true , using : dependencies )
scheduleDownload ( for : owner , currentFileInvalid : true )
return nil
}
@ -114,23 +126,22 @@ public struct DisplayPictureManager {
return data
}
public static func loadDisplayPictureFromDisk ( for fileName : String , using dependencies : Dependencies ) -> Data ? {
guard let filePath : String = try ? DisplayPictureManager . filepath ( for : fileName , using : dependencies ) else {
return nil
}
public func loadDisplayPictureFromDisk ( for fileName : String ) -> Data ? {
guard let filePath : String = try ? filepath ( for : fileName ) else { return nil }
return try ? Data ( contentsOf : URL ( fileURLWithPath : filePath ) )
}
// MARK: - F i l e P a t h s
public static func profileAvatarFilepath (
public func profileAvatarFilepath (
_ db : Database ? = nil ,
id : String ,
using dependencies : Dependencies
id : String
) -> String ? {
guard let db : Database = db else {
return dependencies [ singleton : . storage ] . read { db in profileAvatarFilepath ( db , id : id , using : dependencies ) }
return dependencies [ singleton : . storage ] . read { [ weak self ] db in
self ? . profileAvatarFilepath ( db , id : id )
}
}
let maybeFileName : String ? = try ? Profile
@ -139,10 +150,10 @@ public struct DisplayPictureManager {
. asRequest ( of : String . self )
. fetchOne ( db )
return maybeFileName . map { try ? DisplayPictureManager. filepath( for : $0 , using : dependencies ) }
return maybeFileName . map { try ? filepath( for : $0 ) }
}
public static func generateFilename ( for url : String , using dependencies : Dependencies ) -> String {
public func generateFilename ( for url : String ) -> String {
return ( dependencies [ singleton : . crypto ]
. generate ( . hash ( message : url . bytes ) ) ?
. toHexString ( ) )
@ -150,7 +161,7 @@ public struct DisplayPictureManager {
. appendingFileExtension ( " jpg " ) // s t r i n g l i n t : i g n o r e
}
public static func generateFilename ( using dependencies : Dependencies ) -> String {
public func generateFilename ( ) -> String {
return dependencies [ singleton : . crypto ]
. generate ( . uuid ( ) )
. defaulting ( to : UUID ( ) )
@ -158,91 +169,76 @@ public struct DisplayPictureManager {
. appendingFileExtension ( " jpg " ) // s t r i n g l i n t : i g n o r e
}
public static func filepath ( for filename : String , using dependencies : Dependencies ) throws -> String {
public func filepath ( for filename : String ) throws -> String {
guard ! filename . isEmpty else { throw DisplayPictureError . invalidCall }
return URL ( fileURLWithPath : sharedDataDisplayPictureDirPath ( using : dependencies ) )
return URL ( fileURLWithPath : sharedDataDisplayPictureDirPath ( ) )
. appendingPathComponent ( filename )
. path
}
public static func resetStorage ( using dependencies : Dependencies ) {
public func resetStorage ( ) {
try ? dependencies [ singleton : . fileManager ] . removeItem (
atPath : DisplayPictureManager. sharedDataDisplayPictureDirPath( using : dependencies )
atPath : sharedDataDisplayPictureDirPath( )
)
}
// MARK: - D o w n l o a d i n g
private static func scheduleDownload (
for owner : Owner ,
currentFileInvalid invalid : Bool ,
using dependencies : Dependencies
) {
dependencies . mutate ( cache : . displayPicture ) { cache in
cache . downloadsToSchedule . insert ( DownloadInfo ( owner : owner , currentFileInvalid : invalid ) )
}
// / T h i s m e t h o d c a n b e t r i g g e r e d v e r y f r e q u e n t l y w h e n p r o c e s s i n g m e s s a g e s s o w e w a n t t o t h r o t t l e t h e u p d a t e s t o 2 5 0 m s ( i t ' s f o r s t a r t i n g
// / a v a t a r d o w n l o a d s s o t h a t s h o u l d d e f i n i t e l y b e f a s t e n o u g h )
if scheduleDownloadsPublisher = = nil {
scheduleDownloadsPublisher = scheduleDownloadsTrigger
. throttle ( for : . milliseconds ( 250 ) , scheduler : DispatchQueue . global ( qos : . userInitiated ) , latest : true )
. handleEvents (
receiveOutput : { [ dependencies ] _ in
let pendingInfo : Set < DownloadInfo > = dependencies . mutate ( cache : . displayPicture ) { cache in
let result : Set < DownloadInfo > = cache . downloadsToSchedule
cache . downloadsToSchedule . removeAll ( )
return result
}
dependencies [ singleton : . storage ] . writeAsync { db in
pendingInfo . forEach { info in
// I f t h e c u r r e n t f i l e i s i n v a l i d t h e n c l e a r o u t t h e ' p r o f i l e P i c t u r e F i l e N a m e '
// a n d t r y t o r e - d o w n l o a d t h e f i l e
if info . currentFileInvalid {
info . owner . clearCurrentFile ( db )
}
dependencies [ singleton : . jobRunner ] . add (
db ,
job : Job (
variant : . displayPictureDownload ,
shouldBeUnique : true ,
details : DisplayPictureDownloadJob . Details ( owner : info . owner )
) ,
canStartJob : true
)
// / P r o f i l e p i c t u r e d o w n l o a d s c a n b e t r i g g e r e d v e r y f r e q u e n t l y w h e n p r o c e s s i n g m e s s a g e s s o w e w a n t t o t h r o t t l e t h e u p d a t e s t o
// / 2 5 0 m s ( i t ' s f o r s t a r t i n g a v a t a r d o w n l o a d s s o t h a t s h o u l d d e f i n i t e l y b e f a s t e n o u g h )
private func setupThrottledDownloading ( ) {
scheduleDownloadsCancellable = scheduleDownloads
. throttle ( for : . milliseconds ( 250 ) , scheduler : DispatchQueue . global ( qos : . userInitiated ) , latest : true )
. sink (
receiveValue : { [ dependencies ] _ in
let pendingInfo : Set < DownloadInfo > = dependencies . mutate ( cache : . displayPicture ) { cache in
let result : Set < DownloadInfo > = cache . downloadsToSchedule
cache . downloadsToSchedule . removeAll ( )
return result
}
dependencies [ singleton : . storage ] . writeAsync { db in
pendingInfo . forEach { info in
// I f t h e c u r r e n t f i l e i s i n v a l i d t h e n c l e a r o u t t h e ' p r o f i l e P i c t u r e F i l e N a m e '
// a n d t r y t o r e - d o w n l o a d t h e f i l e
if info . currentFileInvalid {
info . owner . clearCurrentFile ( db )
}
dependencies [ singleton : . jobRunner ] . add (
db ,
job : Job (
variant : . displayPictureDownload ,
shouldBeUnique : true ,
details : DisplayPictureDownloadJob . Details ( owner : info . owner )
) ,
canStartJob : true
)
}
}
)
. map { _ in ( ) }
. eraseToAnyPublisher ( )
scheduleDownloadsPublisher ? . sinkUntilComplete ( )
}
)
}
private func scheduleDownload ( for owner : Owner , currentFileInvalid invalid : Bool ) {
dependencies . mutate ( cache : . displayPicture ) { cache in
cache . downloadsToSchedule . insert ( DownloadInfo ( owner : owner , currentFileInvalid : invalid ) )
}
scheduleDownloadsTrigger . send ( ( ) )
scheduleDownloads . send ( ( ) )
}
// MARK: - U p l o a d i n g
public static func prepareAndUploadDisplayPicture (
queue : DispatchQueue ,
imageData : Data ,
success : @ escaping ( ( downloadUrl : String , fileName : String , encryptionKey : Data ) ) -> ( ) ,
failure : ( ( DisplayPictureError ) -> ( ) ) ? = nil ,
using dependencies : Dependencies
) {
queue . async ( using : dependencies ) {
// I f t h e p r o f i l e a v a t a r w a s u p d a t e d o r r e m o v e d t h e n e n c r y p t w i t h a n e w p r o f i l e k e y
// t o e n s u r e t h a t o t h e r u s e r s k n o w t h a t o u r p r o f i l e p i c t u r e w a s u p d a t e d
let newEncryptionKey : Data
let finalImageData : Data
let fileExtension : String
do {
public func prepareAndUploadDisplayPicture ( imageData : Data ) -> AnyPublisher < UploadResult , DisplayPictureError > {
return Just ( ( ) )
. setFailureType ( to : DisplayPictureError . self )
. tryMap { [ weak self , dependencies ] _ -> ( Network . PreparedRequest < FileUploadResponse > , String , Data , Data ) in
// I f t h e p r o f i l e a v a t a r w a s u p d a t e d o r r e m o v e d t h e n e n c r y p t w i t h a n e w p r o f i l e k e y
// t o e n s u r e t h a t o t h e r u s e r s k n o w t h a t o u r p r o f i l e p i c t u r e w a s u p d a t e d
let newEncryptionKey : Data
let finalImageData : Data
let fileExtension : String
let guessedFormat : ImageFormat = imageData . guessedImageFormat
finalImageData = try {
@ -301,80 +297,83 @@ public struct DisplayPictureManager {
default : return " jpg " // s t r i n g l i n t : i g n o r e
}
} ( )
// I f w e h a v e a n e w a v a t a r i m a g e , w e m u s t f i r s t :
//
// * W r i t e i t t o d i s k .
// * E n c r y p t i t
// * U p l o a d i t t o a s s e t s e r v i c e
// * S e n d a s s e t s e r v i c e i n f o t o S i g n a l S e r v i c e
Log . verbose ( . displayPictureManager , " Updating local profile on service with new avatar. " )
let fileName : String = dependencies [ singleton : . crypto ] . generate ( . uuid ( ) )
. defaulting ( to : UUID ( ) )
. uuidString
. appendingFileExtension ( fileExtension )
guard let filePath : String = try ? self ? . filepath ( for : fileName ) else {
throw DisplayPictureError . invalidFilename
}
// W r i t e t h e a v a t a r t o d i s k
do { try finalImageData . write ( to : URL ( fileURLWithPath : filePath ) , options : [ . atomic ] ) }
catch {
Log . error ( . displayPictureManager , " Updating service with profile failed. " )
throw DisplayPictureError . writeFailed
}
// E n c r y p t t h e a v a t a r f o r u p l o a d
guard
let encryptedData : Data = dependencies [ singleton : . crypto ] . generate (
. encryptedDataDisplayPicture ( data : finalImageData , key : newEncryptionKey , using : dependencies )
)
else {
Log . error ( . displayPictureManager , " Updating service with profile failed. " )
throw DisplayPictureError . encryptionFailed
}
// U p l o a d t h e a v a t a r t o t h e F i l e S e r v e r
guard
let preparedUpload : Network . PreparedRequest < FileUploadResponse > = try ? Network . preparedUpload (
data : encryptedData ,
requestAndPathBuildTimeout : Network . fileUploadTimeout ,
using : dependencies
)
else {
Log . error ( . displayPictureManager , " Updating service with profile failed. " )
throw DisplayPictureError . uploadFailed
}
return ( preparedUpload , fileName , newEncryptionKey , finalImageData )
}
catch let error as DisplayPictureError { return ( failure ? ( error ) ? ? { } ( ) ) }
catch { return ( failure ? ( DisplayPictureError . invalidCall ) ? ? { } ( ) ) }
// I f w e h a v e a n e w a v a t a r i m a g e , w e m u s t f i r s t :
//
// * W r i t e i t t o d i s k .
// * E n c r y p t i t
// * U p l o a d i t t o a s s e t s e r v i c e
// * S e n d a s s e t s e r v i c e i n f o t o S i g n a l S e r v i c e
Log . verbose ( . displayPictureManager , " Updating local profile on service with new avatar. " )
let fileName : String = dependencies [ singleton : . crypto ] . generate ( . uuid ( ) )
. defaulting ( to : UUID ( ) )
. uuidString
. appendingFileExtension ( fileExtension )
guard let filePath : String = try ? DisplayPictureManager . filepath ( for : fileName , using : dependencies ) else {
failure ? ( . invalidFilename )
return
}
// W r i t e t h e a v a t a r t o d i s k
do { try finalImageData . write ( to : URL ( fileURLWithPath : filePath ) , options : [ . atomic ] ) }
catch {
Log . error ( . displayPictureManager , " Updating service with profile failed. " )
failure ? ( . writeFailed )
return
. flatMap { [ dependencies ] preparedUpload , fileName , newEncryptionKey , finalImageData -> AnyPublisher < ( FileUploadResponse , String , Data , Data ) , Error > in
preparedUpload . send ( using : dependencies )
. map { _ , response -> ( FileUploadResponse , String , Data , Data ) in
( response , fileName , newEncryptionKey , finalImageData )
}
. eraseToAnyPublisher ( )
}
// E n c r y p t t h e a v a t a r f o r u p l o a d
guard
let encryptedData : Data = dependencies [ singleton : . crypto ] . generate (
. encryptedDataDisplayPicture ( data : finalImageData , key : newEncryptionKey , using : dependencies )
)
else {
Log . error ( . displayPictureManager , " Updating service with profile failed. " )
failure ? ( . encryptionFailed )
return
. mapError { error in
Log . error ( . displayPictureManager , " Updating service with profile failed with error: \( error ) . " )
switch error {
case NetworkError . maxFileSizeExceeded : return DisplayPictureError . uploadMaxFileSizeExceeded
case let displayPictureError as DisplayPictureError : return displayPictureError
default : return DisplayPictureError . uploadFailed
}
}
// U p l o a d t h e a v a t a r t o t h e F i l e S e r v e r
guard let preparedUpload : Network . PreparedRequest < FileUploadResponse > = try ? Network . preparedUpload ( data : encryptedData , using : dependencies ) else {
Log . error ( . displayPictureManager , " Updating service with profile failed. " )
failure ? ( . uploadFailed )
return
. map { [ dependencies ] fileUploadResponse , fileName , newEncryptionKey , finalImageData -> UploadResult in
let downloadUrl : String = Network . FileServer . downloadUrlString ( for : fileUploadResponse . id )
// U p d a t e t h e c a c h e d a v a t a r i m a g e v a l u e
dependencies . mutate ( cache : . displayPicture ) {
$0 . imageData [ fileName ] = finalImageData
}
Log . verbose ( . displayPictureManager , " Successfully uploaded avatar image. " )
return ( downloadUrl , fileName , newEncryptionKey )
}
preparedUpload
. send ( using : dependencies )
. subscribe ( on : DispatchQueue . global ( qos : . userInitiated ) , using : dependencies )
. receive ( on : queue , using : dependencies )
. sinkUntilComplete (
receiveCompletion : { result in
switch result {
case . finished : break
case . failure ( let error ) :
Log . error ( . displayPictureManager , " Updating service with profile failed with error: \( error ) . " )
let isMaxFileSizeExceeded : Bool = ( ( error as ? NetworkError ) = = . maxFileSizeExceeded )
failure ? ( isMaxFileSizeExceeded ? . uploadMaxFileSizeExceeded : . uploadFailed )
}
} ,
receiveValue : { _ , fileUploadResponse in
let downloadUrl : String = Network . FileServer . downloadUrlString ( for : fileUploadResponse . id )
// U p d a t e t h e c a c h e d a v a t a r i m a g e v a l u e
dependencies . mutate ( cache : . displayPicture ) { $0 . imageData [ fileName ] = finalImageData }
Log . verbose ( . displayPictureManager , " Successfully uploaded avatar image. " )
success ( ( downloadUrl , fileName , newEncryptionKey ) )
}
)
}
. eraseToAnyPublisher ( )
}
}