diff --git a/Podfile b/Podfile index f20ca96..7cd4398 100644 --- a/Podfile +++ b/Podfile @@ -5,6 +5,7 @@ target 'RxFirebase_Example' do pod 'RxFirebase/Firestore', :path => './' pod 'RxFirebase/RemoteConfig', :path => './' pod 'RxFirebase/Database', :path => './' + pod 'RxFirebase/Storage', :path => './' target 'RxFirebase_Tests' do inherit! :search_paths diff --git a/Podfile.lock b/Podfile.lock index ce9e61d..03ee150 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -38,6 +38,9 @@ PODS: - FirebaseInstanceID (~> 3.0) - "GoogleToolboxForMac/NSData+zlib (~> 2.1)" - Protobuf (~> 3.5) + - FirebaseStorage (3.0.0): + - FirebaseCore (~> 5.0) + - GTMSessionFetcher/Core (~> 1.1) - GoogleToolboxForMac/Defines (2.1.4) - "GoogleToolboxForMac/NSData+zlib (2.1.4)": - GoogleToolboxForMac/Defines (= 2.1.4) @@ -60,6 +63,7 @@ PODS: - gRPC/Main (1.11.0): - gRPC-Core (= 1.11.0) - gRPC-RxLibrary (= 1.11.0) + - GTMSessionFetcher/Core (1.1.15) - leveldb-library (1.20) - nanopb (0.3.8): - nanopb/decode (= 0.3.8) @@ -69,24 +73,29 @@ PODS: - Protobuf (3.5.0) - RxCocoa (4.1.2): - RxSwift (~> 4.0) - - RxFirebase/Database (0.2): + - RxFirebase/Database (0.2.1): - FirebaseDatabase (~> 5) - RxCocoa (~> 4) - RxSwift (~> 4) - - RxFirebase/Firestore (0.2): + - RxFirebase/Firestore (0.2.1): - FirebaseFirestore (~> 0.12) - RxCocoa (~> 4) - RxSwift (~> 4) - - RxFirebase/RemoteConfig (0.2): + - RxFirebase/RemoteConfig (0.2.1): - FirebaseRemoteConfig (~> 3) - RxCocoa (~> 4) - RxSwift (~> 4) + - RxFirebase/Storage (0.2.1): + - FirebaseStorage (~> 3) + - RxCocoa (~> 4) + - RxSwift (~> 4) - RxSwift (4.1.2) DEPENDENCIES: - RxFirebase/Database (from `./`) - RxFirebase/Firestore (from `./`) - RxFirebase/RemoteConfig (from `./`) + - RxFirebase/Storage (from `./`) SPEC REPOS: https://github.com/cocoapods/specs.git: @@ -98,11 +107,13 @@ SPEC REPOS: - FirebaseFirestore - FirebaseInstanceID - FirebaseRemoteConfig + - FirebaseStorage - GoogleToolboxForMac - gRPC - gRPC-Core - gRPC-ProtoRPC - gRPC-RxLibrary + - GTMSessionFetcher - leveldb-library - nanopb - Protobuf @@ -122,18 +133,20 @@ SPEC CHECKSUMS: FirebaseFirestore: f686b8e83f3cf8bbc37db6e98e01029a14f01f55 FirebaseInstanceID: 83e0040351565df711a5db3d8ebe5ea21aca998a FirebaseRemoteConfig: 3c57e4644bd6976b671ae0b725cd709f198bd1f5 + FirebaseStorage: 7ca4bb7b58a25fa647b04f524033fc7cb7eb272b GoogleToolboxForMac: 91c824d21e85b31c2aae9bb011c5027c9b4e738f gRPC: 70703dc9ba31c72341fc7f37745cc1c379edee96 gRPC-Core: 164639cd8ae18ca8b65477fafb2efbaecf4f181a gRPC-ProtoRPC: bb5fddf3424aa4fad74d76736578a79fe40e244e gRPC-RxLibrary: 26d53d1b1f306befd4ad4e15bd6de27839a82481 + GTMSessionFetcher: 5fa5b80fd20e439ef5f545fb2cb3ca6c6714caa2 leveldb-library: 08cba283675b7ed2d99629a4bc5fd052cd2bb6a5 nanopb: 5601e6bca2dbf1ed831b519092ec110f66982ca3 Protobuf: 8a9838fba8dae3389230e1b7f8c104aa32389c03 RxCocoa: d88ba0f1f6abf040011a9eb4b539324fc426843a - RxFirebase: 564de37ff7429fb88313635d8a4b16c656bd9bbd + RxFirebase: 25a2fd31fe5795d90338fba7beb9b415209a215e RxSwift: e49536837d9901277638493ea537394d4b55f570 -PODFILE CHECKSUM: fde8e8f30c4a2721a77fa8212056b41e090fe118 +PODFILE CHECKSUM: 973a2757330c974ebe33f0c1e979ded68c8ec05a -COCOAPODS: 1.5.2 +COCOAPODS: 1.5.3 diff --git a/README.md b/README.md index 0cb9342..204f5a8 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ it, simply add the following line to your Podfile: pod 'RxFirebase/Firestore' pod 'RxFirebase/RemoteConfig' pod 'RxFirebase/Database' +pod 'RxFirebase/Storage' ``` ## Usage @@ -31,6 +32,7 @@ import RxFirebase - [Database](#database) - [Firestore](#firestore) - [RemoteConfig](#remoteconfig) +- [Storage](#storage) ### Database @@ -300,6 +302,138 @@ RemoteConfig.remoteConfig() // https://firebase.google.com/docs/remote-config/ios ``` +### Storage + +Upload: +```swift + +let reference = Storage.storage() + .reference(forURL: "\(your_firebase_storage_bucket)/images/space.jpg") + .rx + +let data: Data // Upload data +reference.putData(data) + .subscribe(onNext: { metadata in + // Success + }, onError: { error in + // Uh-oh, an error occurred! + }).disposed(by: disposeBag) + + +let fileURL: URL // Upload file +reference.putFile(from: fileURL) + .subscribe(onNext: { metadata in + // Success + }, onError: { error in + // Uh-oh, an error occurred! + }).disposed(by: disposeBag) +``` + +Observe events: +```swift +let reference = Storage.storage() + .reference(forURL: "\(your_firebase_storage_bucket)/images/space.jpg") + .rx + +let fileURL: URL // Upload file +let uploadTask = reference.putFile(from: fileURL) + +// Listen for state changes +task.rx.observe(.progress) + .subscribe(onNext: { snapshot in + // Upload reported progress + let percentComplete = 100.0 * Double(snapshot.progress!.completedUnitCount) + / Double(snapshot.progress!.totalUnitCount) + }, onError: { error in + // Uh-oh, an error occurred! + }).disposed(by: disposeBag) +``` + +Download: +```swift +let reference = Storage.storage() + .reference(forURL: "\(your_firebase_storage_bucket)/images/space.jpg") + .rx + +// Download in memory with a maximum allowed size of 1MB (1 * 1024 * 1024 bytes) +reference.getData(maxSize: 1 * 1024 * 1024) + .subscribe(onNext: { data in + // Data for "images/space.jpg" is returned + }, onError: { error in + // Uh-oh, an error occurred! + }).disposed(by: disposeBag) + + + +// Create local filesystem URL +let localURL = URL(string: "path/to/image")! + +// Download to the local filesystem +reference.write(toFile: localURL) + .subscribe(onNext: { data in + // Local file URL for "images/space.jpg" is returned + }, onError: { error in + // Uh-oh, an error occurred! + }).disposed(by: disposeBag) +``` + +URL: +```swift +let reference = Storage.storage() + .reference(forURL: "\(your_firebase_storage_bucket)/images/space.jpg") + .rx + +// Fetch the download URL +reference.downloadURL() + .subscribe(onNext: { url in + // Get the download URL for 'images/space.jpg' + }, onError: { error in + // Handle any errors + }).disposed(by: disposeBag) +``` + +Metadata: +```swift +let reference = Storage.storage() + .reference(forURL: "\(your_firebase_storage_bucket)/images/space.jpg") + .rx + +// Create file metadata to update +let newMetadata = StorageMetadata() + +// Update metadata properties +reference.updateMetadata(newMetadata) + .subscribe(onNext: { metadata in + // Updated metadata for 'images/space.jpg' is returned + }, onError: { error in + // Uh-oh, an error occurred! + }).disposed(by: disposeBag) + + +// Get metadata properties +reference.getMetadata() + .subscribe(onNext: { metadata in + // Metadata now contains the metadata for 'images/space.jpg' + }, onError: { error in + // Uh-oh, an error occurred! + }).disposed(by: disposeBag) +``` + +Delete: +```swift +let reference = Storage.storage() + .reference(forURL: "\(your_firebase_storage_bucket)/images/space.jpg") + .rx + +// Delete the file +reference.delete() + .subscribe(onNext: { + // File deleted successfully + }, onError: { error in + // Uh-oh, an error occurred! + }).disposed(by: disposeBag) +``` + ## License This library belongs to _RxSwiftCommunity_. diff --git a/RxFirebase.podspec b/RxFirebase.podspec index 2622c3d..365b0d8 100644 --- a/RxFirebase.podspec +++ b/RxFirebase.podspec @@ -48,4 +48,8 @@ Pod::Spec.new do |s| database.source_files = 'Sources/Database/**/*' database.dependency 'FirebaseDatabase', '~> 5' end + s.subspec 'Storage' do |storage| + storage.source_files = 'Sources/Storage/**/*' + storage.dependency 'FirebaseStorage', '~> 3' + end end diff --git a/RxFirebase.xcodeproj/project.pbxproj b/RxFirebase.xcodeproj/project.pbxproj index 9a682e9..6a6454b 100644 --- a/RxFirebase.xcodeproj/project.pbxproj +++ b/RxFirebase.xcodeproj/project.pbxproj @@ -305,6 +305,7 @@ inputPaths = ( "${SRCROOT}/Pods/Target Support Files/Pods-RxFirebase_Example/Pods-RxFirebase_Example-frameworks.sh", "${BUILT_PRODUCTS_DIR}/BoringSSL/openssl.framework", + "${BUILT_PRODUCTS_DIR}/GTMSessionFetcher/GTMSessionFetcher.framework", "${BUILT_PRODUCTS_DIR}/GoogleToolboxForMac/GoogleToolboxForMac.framework", "${BUILT_PRODUCTS_DIR}/Protobuf/Protobuf.framework", "${BUILT_PRODUCTS_DIR}/RxCocoa/RxCocoa.framework", @@ -319,6 +320,7 @@ name = "[CP] Embed Pods Frameworks"; outputPaths = ( "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/openssl.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GTMSessionFetcher.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GoogleToolboxForMac.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Protobuf.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RxCocoa.framework", diff --git a/Sources/Storage/FIRStorageObservableTask+Rx.swift b/Sources/Storage/FIRStorageObservableTask+Rx.swift new file mode 100644 index 0000000..76dbb69 --- /dev/null +++ b/Sources/Storage/FIRStorageObservableTask+Rx.swift @@ -0,0 +1,30 @@ +// +// FIRStorageObservableTask+Rx.swift +// RxFirebase +// +// Created by Arnaud Dorgans on 19/07/2018. +// + +import UIKit +import RxSwift +import FirebaseStorage + +extension Reactive where Base: StorageObservableTask { + + /** + * Observes changes in the upload status: Resume, Pause, Progress, Success, and Failure. + * @param status The FIRStorageTaskStatus change to observe. + * @param handler A callback that fires every time the status event occurs, + * returns a FIRStorageTaskSnapshot containing the state of the task. + */ + public func observe(_ status: StorageTaskStatus) -> Observable { + return Observable.create { observer in + let handle = self.base.observe(status) { snapshot in + observer.onNext(snapshot) + } + return Disposables.create { + self.base.removeObserver(withHandle: handle) + } + } + } +} diff --git a/Sources/Storage/FIRStorageReference+Rx.swift b/Sources/Storage/FIRStorageReference+Rx.swift new file mode 100644 index 0000000..626eaea --- /dev/null +++ b/Sources/Storage/FIRStorageReference+Rx.swift @@ -0,0 +1,201 @@ +// +// FIRStorageReference+Rx.swift +// RxFirebase +// +// Created by Arnaud Dorgans on 19/07/2018. +// + +import RxSwift +import FirebaseStorage + +extension Reactive where Base: StorageReference { + + /** + * Asynchronously uploads data to the currently specified FIRStorageReference. + * This is not recommended for large files, and one should instead upload a file from disk. + * @param uploadData The NSData to upload. + * @param metadata FIRStorageMetadata containing additional information (MIME type, etc.) + * about the object being uploaded. + * @param completion A completion block that either returns the object metadata on success, + * or an error on failure. + */ + public func putData(_ uploadData: Data, metadata: StorageMetadata? = nil) -> Observable { + return Observable.create { observer in + let task = self.base.putData(uploadData, metadata: metadata) { metadata, error in + guard let error = error else { + if let metadata = metadata { + observer.onNext(metadata) + } + observer.onCompleted() + return + } + observer.onError(error) + } + return Disposables.create { + task.cancel() + } + } + } + + /** + * Asynchronously uploads a file to the currently specified FIRStorageReference. + * @param fileURL A URL representing the system file path of the object to be uploaded. + * @param metadata FIRStorageMetadata containing additional information (MIME type, etc.) + * about the object being uploaded. + * @param completion A completion block that either returns the object metadata on success, + * or an error on failure. + */ + public func putFile(from url: URL, metadata: StorageMetadata? = nil) -> Observable { + return Observable.create { observer in + let task = self.base.putFile(from: url, metadata: metadata) { metadata, error in + guard let error = error else { + if let metadata = metadata { + observer.onNext(metadata) + } + observer.onCompleted() + return + } + observer.onError(error) + } + return Disposables.create { + task.cancel() + } + } + } + + /** + * Asynchronously downloads the object at the FIRStorageReference to an NSData object in memory. + * An NSData of the provided max size will be allocated, so ensure that the device has enough free + * memory to complete the download. For downloading large files, writeToFile may be a better option. + * @param size The maximum size in bytes to download. If the download exceeds this size + * the task will be cancelled and an error will be returned. + * @param completion A completion block that either returns the object data on success, + * or an error on failure. + */ + public func getData(maxSize: Int64) -> Observable { + return Observable.create { observer in + let task = self.base.getData(maxSize: maxSize) { data, error in + guard let error = error else { + if let data = data { + observer.onNext(data) + } + observer.onCompleted() + return + } + observer.onError(error) + } + return Disposables.create { + task.cancel() + } + } + } + + /** + * Asynchronously retrieves a long lived download URL with a revokable token. + * This can be used to share the file with others, but can be revoked by a developer + * in the Firebase Console if desired. + * @param completion A completion block that either returns the URL on success, + * or an error on failure. + */ + public func downloadURL() -> Observable { + return Observable.create { observer in + self.base.downloadURL { url, error in + guard let error = error else { + if let url = url { + observer.onNext(url) + } + observer.onCompleted() + return + } + observer.onError(error) + } + return Disposables.create() + } + } + + /** + * Asynchronously downloads the object at the current path to a specified system filepath. + * @param fileURL A file system URL representing the path the object should be downloaded to. + * @param completion A completion block that fires when the file download completes. + * Returns an NSURL pointing to the file path of the downloaded file on success, + * or an error on failure. + */ + public func write(toFile url: URL) -> Observable { + return Observable.create { observer in + let task = self.base.write(toFile: url) { url, error in + guard let error = error else { + if let url = url { + observer.onNext(url) + } + observer.onCompleted() + return + } + observer.onError(error) + } + return Disposables.create { + task.cancel() + } + } + } + + /** + * Retrieves metadata associated with an object at the current path. + * @param completion A completion block which returns the object metadata on success, + * or an error on failure. + */ + public func getMetadata() -> Observable { + return Observable.create { observer in + self.base.getMetadata { metadata, error in + guard let error = error else { + if let metadata = metadata { + observer.onNext(metadata) + } + observer.onCompleted() + return + } + observer.onError(error) + } + return Disposables.create() + } + } + + /** + * Updates the metadata associated with an object at the current path. + * @param metadata An FIRStorageMetadata object with the metadata to update. + * @param completion A completion block which returns the FIRStorageMetadata on success, + * or an error on failure. + */ + public func updateMetadata(_ metadata: StorageMetadata) -> Observable { + return Observable.create { observer in + self.base.updateMetadata(metadata) { metadata, error in + guard let error = error else { + if let metadata = metadata { + observer.onNext(metadata) + } + observer.onCompleted() + return + } + observer.onError(error) + } + return Disposables.create() + } + } + + /** + * Deletes the object at the current path. + * @param completion A completion block which returns nil on success, or an error on failure. + */ + public func delete() -> Observable { + return Observable.create { observer in + self.base.delete { error in + guard let error = error else { + observer.onNext(()) + observer.onCompleted() + return + } + observer.onError(error) + } + return Disposables.create() + } + } +}