| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202 |
- // Copyright 2022 Google LLC
- //
- // Licensed under the Apache License, Version 2.0 (the "License");
- // you may not use this file except in compliance with the License.
- // You may obtain a copy of the License at
- //
- // http://www.apache.org/licenses/LICENSE-2.0
- //
- // Unless required by applicable law or agreed to in writing, software
- // distributed under the License is distributed on an "AS IS" BASIS,
- // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- // See the License for the specific language governing permissions and
- // limitations under the License.
- import Foundation
- enum StoragePathError: Error {
- case storagePathError(String)
- }
- /**
- * Represents a path in GCS, which can be represented as: gs://bucket/path/to/object
- * or http[s]://firebasestorage.googleapis.com/v0/b/bucket/o/path/to/object?token=<12345>
- * This class also includes helper methods to parse those URI/Ls, as well as to
- * add and remove path segments.
- */
- internal class StoragePath: NSCopying, Equatable {
- // MARK: Class methods
- /**
- * Parses a generic string (representing some URI or URL) and returns the appropriate path.
- * @param string String which is parsed into a path.
- * @return Returns an instance of StoragePath.
- * @throws Throws an error if the string is not a valid gs:// URI or http[s]:// URL.
- */
- static func path(string: String) throws -> StoragePath {
- if string.hasPrefix("gs://") {
- // "gs://bucket/path/to/object"
- return try path(GSURI: string)
- } else if string.hasPrefix("http://") || string.hasPrefix("https://") {
- // "http[s]://firebasestorage.googleapis.com/bucket/path/to/object?signed_url_params"
- return try path(HTTPURL: string)
- } else {
- // Invalid scheme, throw an error!
- throw StoragePathError.storagePathError("Internal error: URL scheme must be one " +
- "of gs://, http://, or https://")
- }
- }
- /**
- * Parses a gs://bucket/path/to/object URI into a GCS path.
- * @param aURIString gs:// URI which is parsed into a path.
- * @return Returns an instance of StoragePath or nil if one can't be created.
- * @throws Throws an error if the string is not a valid gs:// URI.
- */
- internal static func path(GSURI aURIString: String) throws -> StoragePath {
- if aURIString.starts(with: "gs://") {
- let bucketObject = aURIString.dropFirst("gs://".count)
- if bucketObject.contains("/") {
- let splitStringArray = bucketObject.split(separator: "/", maxSplits: 1).map(String.init)
- let object = splitStringArray.count == 2 ? splitStringArray[1] : nil
- return StoragePath(with: splitStringArray[0], object: object)
- } else if bucketObject.count > 0 {
- return StoragePath(with: String(bucketObject))
- }
- }
- throw StoragePathError.storagePathError("Internal error: URI must be in the form of " +
- "gs://<bucket>/<path/to/object>")
- }
- /**
- * Parses a http[s]://firebasestorage.googleapis.com/v0/b/bucket/o/path/to/object...?token=<12345>
- * URL into a GCS path.
- * @param aURLString http[s]:// URL which is parsed into a path.
- * string which is parsed into a path.
- * @return Returns an instance of StoragePath or nil if one can't be created.
- * @throws Throws an error if the string is not a valid http[s]:// URL.
- */
- private static func path(HTTPURL aURLString: String) throws -> StoragePath {
- let httpsURL = URL(string: aURLString)
- let pathComponents = httpsURL?.pathComponents
- guard let pathComponents = pathComponents,
- pathComponents.count >= 4,
- pathComponents[1] == "v0",
- pathComponents[2] == "b" else {
- throw StoragePathError.storagePathError("Internal error: URL must be in the form of " +
- "http[s]://<host>/v0/b/<bucket>/o/<path/to/object>[?token=signed_url_params]")
- }
- let bucketName = pathComponents[3]
- guard pathComponents.count > 4 else {
- return StoragePath(with: bucketName)
- }
- // Construct object name
- var objectName = pathComponents[5]
- for i in 6 ..< pathComponents.count {
- objectName = "\(objectName)/\(pathComponents[i])"
- }
- return StoragePath(with: bucketName, object: objectName)
- }
- // Removes leading and trailing slashes, and compresses multiple slashes
- // to create a canonical representation.
- // Example: /foo//bar///baz//// -> foo/bar/baz
- private static func standardizedPathForString(_ string: String) -> String {
- var output = string
- while true {
- let newOutput = output.replacingOccurrences(of: "//", with: "/")
- if newOutput == output {
- break
- }
- output = newOutput
- }
- return output.trimmingCharacters(in: ["/"])
- }
- // MARK: - Internal Implementations
- /**
- * The GCS bucket in the path.
- */
- internal let bucket: String
- /**
- * The GCS object in the path.
- */
- internal let object: String?
- /**
- * Constructs an StoragePath object that represents the given bucket and object.
- * @param bucket The name of the bucket.
- * @param object The name of the object.
- * @return An instance of StoragePath representing the @a bucket and @a object.
- */
- internal init(with bucket: String,
- object: String? = nil) {
- self.bucket = bucket
- if let object = object {
- self.object = StoragePath.standardizedPathForString(object)
- } else {
- self.object = nil
- }
- }
- static func == (lhs: StoragePath, rhs: StoragePath) -> Bool {
- return lhs.bucket == rhs.bucket && lhs.object == rhs.object
- }
- internal func copy(with zone: NSZone? = nil) -> Any {
- return StoragePath(with: bucket, object: object)
- }
- /**
- * Creates a new path based off of the current path and a string appended to it.
- * Note that all slashes are compressed to a single slash, and leading and trailing slashes
- * are removed.
- * @param path String to append to the current path.
- * @return Returns a new instance of StoragePath with the new path appended.
- */
- internal func child(_ path: String) -> StoragePath {
- if path.count == 0 {
- return copy() as! StoragePath
- }
- var childObject: String
- if let object = object as? NSString {
- childObject = object.appendingPathComponent(path)
- } else {
- childObject = path
- }
- return StoragePath(with: bucket, object: childObject)
- }
- /**
- * Creates a new path based off of the current path with the last path segment removed.
- * @return Returns a new instance of StoragePath pointing to the parent path,
- * or nil if the current path points to the root.
- */
- internal func parent() -> StoragePath? {
- guard let object = object,
- object.count > 0 else {
- return nil
- }
- let parentObject = (object as NSString).deletingLastPathComponent
- return StoragePath(with: bucket, object: parentObject)
- }
- /**
- * Creates a new path based off of the root of the bucket.
- * @return Returns a new instance of StoragePath pointing to the root of the bucket.
- */
- internal func root() -> StoragePath {
- return StoragePath(with: bucket)
- }
- /**
- * Returns a GS URI representing the current path.
- * @return Returns a gs://bucket/path/to/object URI representing the current path.
- */
- internal func stringValue() -> String {
- return "gs://\(bucket)/\(object ?? "")"
- }
- }
|