StoragePath.swift 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202
  1. // Copyright 2022 Google LLC
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. import Foundation
  15. enum StoragePathError: Error {
  16. case storagePathError(String)
  17. }
  18. /**
  19. * Represents a path in GCS, which can be represented as: gs://bucket/path/to/object
  20. * or http[s]://firebasestorage.googleapis.com/v0/b/bucket/o/path/to/object?token=<12345>
  21. * This class also includes helper methods to parse those URI/Ls, as well as to
  22. * add and remove path segments.
  23. */
  24. class StoragePath: NSCopying, Equatable {
  25. // MARK: Class methods
  26. /**
  27. * Parses a generic string (representing some URI or URL) and returns the appropriate path.
  28. * @param string String which is parsed into a path.
  29. * @return Returns an instance of StoragePath.
  30. * @throws Throws an error if the string is not a valid gs:// URI or http[s]:// URL.
  31. */
  32. static func path(string: String) throws -> StoragePath {
  33. if string.hasPrefix("gs://") {
  34. // "gs://bucket/path/to/object"
  35. return try path(GSURI: string)
  36. } else if string.hasPrefix("http://") || string.hasPrefix("https://") {
  37. // "http[s]://firebasestorage.googleapis.com/bucket/path/to/object?signed_url_params"
  38. return try path(HTTPURL: string)
  39. } else {
  40. // Invalid scheme, throw an error!
  41. throw StoragePathError.storagePathError("Internal error: URL scheme must be one " +
  42. "of gs://, http://, or https://")
  43. }
  44. }
  45. /**
  46. * Parses a gs://bucket/path/to/object URI into a GCS path.
  47. * @param aURIString gs:// URI which is parsed into a path.
  48. * @return Returns an instance of StoragePath or nil if one can't be created.
  49. * @throws Throws an error if the string is not a valid gs:// URI.
  50. */
  51. static func path(GSURI aURIString: String) throws -> StoragePath {
  52. if aURIString.starts(with: "gs://") {
  53. let bucketObject = aURIString.dropFirst("gs://".count)
  54. if bucketObject.contains("/") {
  55. let splitStringArray = bucketObject.split(separator: "/", maxSplits: 1).map(String.init)
  56. let object = splitStringArray.count == 2 ? splitStringArray[1] : nil
  57. return StoragePath(with: splitStringArray[0], object: object)
  58. } else if bucketObject.count > 0 {
  59. return StoragePath(with: String(bucketObject))
  60. }
  61. }
  62. throw StoragePathError.storagePathError("Internal error: URI must be in the form of " +
  63. "gs://<bucket>/<path/to/object>")
  64. }
  65. /**
  66. * Parses a http[s]://firebasestorage.googleapis.com/v0/b/bucket/o/path/to/object...?token=<12345>
  67. * URL into a GCS path.
  68. * @param aURLString http[s]:// URL which is parsed into a path.
  69. * string which is parsed into a path.
  70. * @return Returns an instance of StoragePath or nil if one can't be created.
  71. * @throws Throws an error if the string is not a valid http[s]:// URL.
  72. */
  73. private static func path(HTTPURL aURLString: String) throws -> StoragePath {
  74. let httpsURL = URL(string: aURLString)
  75. let pathComponents = httpsURL?.pathComponents
  76. guard let pathComponents = pathComponents,
  77. pathComponents.count >= 4,
  78. pathComponents[1] == "v0",
  79. pathComponents[2] == "b" else {
  80. throw StoragePathError.storagePathError("Internal error: URL must be in the form of " +
  81. "http[s]://<host>/v0/b/<bucket>/o/<path/to/object>[?token=signed_url_params]")
  82. }
  83. let bucketName = pathComponents[3]
  84. guard pathComponents.count > 4 else {
  85. return StoragePath(with: bucketName)
  86. }
  87. // Construct object name
  88. var objectName = pathComponents[5]
  89. for i in 6 ..< pathComponents.count {
  90. objectName = "\(objectName)/\(pathComponents[i])"
  91. }
  92. return StoragePath(with: bucketName, object: objectName)
  93. }
  94. // Removes leading and trailing slashes, and compresses multiple slashes
  95. // to create a canonical representation.
  96. // Example: /foo//bar///baz//// -> foo/bar/baz
  97. private static func standardizedPathForString(_ string: String) -> String {
  98. var output = string
  99. while true {
  100. let newOutput = output.replacingOccurrences(of: "//", with: "/")
  101. if newOutput == output {
  102. break
  103. }
  104. output = newOutput
  105. }
  106. return output.trimmingCharacters(in: ["/"])
  107. }
  108. // MARK: - Internal Implementations
  109. /**
  110. * The GCS bucket in the path.
  111. */
  112. let bucket: String
  113. /**
  114. * The GCS object in the path.
  115. */
  116. let object: String?
  117. /**
  118. * Constructs an StoragePath object that represents the given bucket and object.
  119. * @param bucket The name of the bucket.
  120. * @param object The name of the object.
  121. * @return An instance of StoragePath representing the @a bucket and @a object.
  122. */
  123. init(with bucket: String,
  124. object: String? = nil) {
  125. self.bucket = bucket
  126. if let object {
  127. self.object = StoragePath.standardizedPathForString(object)
  128. } else {
  129. self.object = nil
  130. }
  131. }
  132. static func == (lhs: StoragePath, rhs: StoragePath) -> Bool {
  133. return lhs.bucket == rhs.bucket && lhs.object == rhs.object
  134. }
  135. func copy(with zone: NSZone? = nil) -> Any {
  136. return StoragePath(with: bucket, object: object)
  137. }
  138. /**
  139. * Creates a new path based off of the current path and a string appended to it.
  140. * Note that all slashes are compressed to a single slash, and leading and trailing slashes
  141. * are removed.
  142. * @param path String to append to the current path.
  143. * @return Returns a new instance of StoragePath with the new path appended.
  144. */
  145. func child(_ path: String) -> StoragePath {
  146. if path.count == 0 {
  147. return copy() as! StoragePath
  148. }
  149. var childObject: String
  150. if let object = object as? NSString {
  151. childObject = object.appendingPathComponent(path)
  152. } else {
  153. childObject = path
  154. }
  155. return StoragePath(with: bucket, object: childObject)
  156. }
  157. /**
  158. * Creates a new path based off of the current path with the last path segment removed.
  159. * @return Returns a new instance of StoragePath pointing to the parent path,
  160. * or nil if the current path points to the root.
  161. */
  162. func parent() -> StoragePath? {
  163. guard let object = object,
  164. object.count > 0 else {
  165. return nil
  166. }
  167. let parentObject = (object as NSString).deletingLastPathComponent
  168. return StoragePath(with: bucket, object: parentObject)
  169. }
  170. /**
  171. * Creates a new path based off of the root of the bucket.
  172. * @return Returns a new instance of StoragePath pointing to the root of the bucket.
  173. */
  174. func root() -> StoragePath {
  175. return StoragePath(with: bucket)
  176. }
  177. /**
  178. * Returns a GS URI representing the current path.
  179. * @return Returns a gs://bucket/path/to/object URI representing the current path.
  180. */
  181. func stringValue() -> String {
  182. return "gs://\(bucket)/\(object ?? "")"
  183. }
  184. }