Skip to content

Commit 0da66c4

Browse files
thisisaaronlandsfomuseumbot
andauthored
Update to use sfomuseum/swift-pmtiles package (#5)
* update to use PMTilesReader * update swift-pmtiles * update to reflect changes to swift-pmtiles * docs --------- Co-authored-by: sfomuseumbot <sfomuseumbot@localhost>
1 parent 8a14e22 commit 0da66c4

File tree

4 files changed

+56
-109
lines changed

4 files changed

+56
-109
lines changed

Package.resolved

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Package.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ import PackageDescription
55

66
let package = Package(
77
name: "swifter-protomaps",
8+
platforms: [
9+
.iOS(.v14),
10+
.macOS(.v11)
11+
],
812
products: [
913
// Products define the executables and libraries a package produces, and make them visible to other packages.
1014
.library(name: "SwifterProtomaps", targets: ["SwifterProtomaps"]),
@@ -13,6 +17,7 @@ let package = Package(
1317
// Dependencies declare other packages that this package depends on.
1418
// .package(url: /* package url */, from: "1.0.0"),
1519
.package(url: "https://github.com/sfomuseum/swifter.git", branch:"main"),
20+
.package(url: "https://github.com/sfomuseum/swift-pmtiles.git", branch:"main"),
1621
.package(url: "https://github.com/apple/swift-log.git", from: "1.6.2"),
1722
.package(url: "https://github.com/apple/swift-argument-parser", from: "1.5.0"),
1823
],
@@ -23,6 +28,7 @@ let package = Package(
2328
name: "SwifterProtomaps",
2429
dependencies: [
2530
.product(name: "Swifter", package: "swifter"),
31+
.product(name: "PMTiles", package: "swift-pmtiles"),
2632
.product(name: "Logging", package: "swift-log")
2733
]),
2834
.testTarget(
@@ -34,6 +40,7 @@ let package = Package(
3440
"SwifterProtomaps",
3541
.product(name: "Swifter", package: "swifter"),
3642
.product(name: "Logging", package: "swift-log"),
43+
.product(name: "PMTiles", package: "swift-pmtiles"),
3744
.product(name: "ArgumentParser", package: "swift-argument-parser")
3845
],
3946
path: "Scripts"

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ This package provides a simple `ServeProtomapsTiles` helper method to serve one
88

99
It was designed for use with an iOS application built around `WKWebKitView` views whose HTML/JavaScript code need to load and render local (on device) Protomaps tiles.
1010

11-
It is not designed to be a general purpose function for serving files using HTTP `Range` requests.
11+
It is not designed to be a general purpose function for serving files using HTTP `Range` requests. It uses the [swift-pmtiles](https://github.com/sfomuseum/swift-pmtiles) package under the hood.
1212

1313
For a longer version detailing why we did this please see the [Serving map tiles to yourself using Protomaps and iOS](https://millsfield.sfomuseum.org/blog/2022/03/30/swifter-protomaps/) blog post.
1414

@@ -167,6 +167,7 @@ This package requires:
167167

168168
## See also
169169

170+
* https://github.com/sfomuseum/swift-pmtiles
170171
* https://github.com/sfomuseum/swifter-protomaps-example
171172
* https://github.com/sfomuseum/swifter
172173
* https://github.com/protomaps/
Lines changed: 38 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import Swifter
22
import Foundation
33
import Logging
4-
import System
4+
import PMTiles
55

66
/// ServeProtomapsOptions defines runtime options for serving Protomaps tiles
77
public struct ServeProtomapsOptions {
@@ -28,8 +28,6 @@ public struct ServeProtomapsOptions {
2828
}
2929

3030
/// ServeProtomapsTiles will serve HTTP range requests for zero or more Protomaps tile databases in a directory.
31-
@available(iOS 14.0, *)
32-
@available(macOS 11.0, *)
3331
public func ServeProtomapsTiles(_ opts: ServeProtomapsOptions) -> ((HttpRequest) -> HttpResponse) {
3432

3533
return { r in
@@ -45,42 +43,7 @@ public func ServeProtomapsTiles(_ opts: ServeProtomapsOptions) -> ((HttpRequest)
4543
if opts.StripPrefix != "" {
4644
rel_path = rel_path.replacingOccurrences(of: opts.StripPrefix, with: "")
4745
}
48-
49-
let uri = opts.Root.appendingPathComponent(rel_path)
50-
let path = uri.absoluteString
51-
52-
// https://developer.apple.com/documentation/foundation/filehandle
53-
54-
var fh: FileHandle?
55-
var fd: FileDescriptor?
56-
57-
do {
58-
59-
if opts.UseFileDescriptor {
60-
let fp = FilePath(uri.absoluteString.replacingOccurrences(of: "file://", with: ""))
61-
fd = try FileDescriptor.open(fp, .readOnly)
62-
} else {
63-
fh = try FileHandle(forReadingFrom: uri)
64-
}
65-
66-
} catch {
67-
opts.Logger?.error("Failed to open path (\(path)) for reading \(error)")
68-
return .raw(404, "Not found", rsp_headers, {_ in })
69-
}
70-
71-
defer {
72-
do {
73-
if opts.UseFileDescriptor {
74-
try fd?.close()
75-
} else {
76-
try fh?.close()
77-
}
7846

79-
} catch (let error) {
80-
opts.Logger?.warning("Failed to close \(path), \(error)")
81-
}
82-
}
83-
8447
guard var range_h = r.headers["range"] else {
8548
rsp_headers["Access-Control-Allow-Origin"] = opts.AllowOrigins
8649
rsp_headers["Access-Control-Allow-Headers"] = opts.AllowHeaders
@@ -107,7 +70,7 @@ public func ServeProtomapsTiles(_ opts: ServeProtomapsOptions) -> ((HttpRequest)
10770
return .raw(400, "Bad Request", rsp_headers, {_ in })
10871
}
10972

110-
guard let stop = Int(positions[1]) else {
73+
guard let stop = UInt64(positions[1]) else {
11174
rsp_headers["X-Error"] = "Invalid stopping range"
11275
return .raw(400, "Bad Request", rsp_headers, {_ in })
11376
}
@@ -117,74 +80,59 @@ public func ServeProtomapsTiles(_ opts: ServeProtomapsOptions) -> ((HttpRequest)
11780
return .raw(400, "Bad Request", rsp_headers, {_ in })
11881
}
11982

120-
opts.Logger?.debug("Read data from \(path) start: \(start) stop: \(stop)")
121-
12283
let next = stop + 1
12384

124-
let body: Data!
85+
// Fetch data
86+
87+
let db_url = opts.Root.appendingPathComponent(rel_path)
88+
89+
var pmtiles_reader: PMTilesReader
12590

12691
do {
12792

128-
opts.Logger?.debug("Seek \(path) to \(start)")
93+
var reader_opts = PMTilesReaderOptions(db_url, use_file_descriptor: opts.UseFileDescriptor)
94+
reader_opts.Logger = opts.Logger
12995

130-
if opts.UseFileDescriptor {
131-
try fd?.seek(offset: Int64(start), from: FileDescriptor.SeekOrigin.start)
132-
} else {
133-
fh?.seek(toFileOffset: start)
134-
}
96+
pmtiles_reader = try PMTilesReader(reader_opts)
13597

13698
} catch {
137-
opts.Logger?.error("Failed to seek to \(start) for \(path), \(error)")
138-
rsp_headers["X-Error"] = "Failed to read from Protomaps tile"
99+
opts.Logger?.error("Failed to instantiate PMTiles reader \(error)")
100+
rsp_headers["X-Error"] = "Failed to instantiate PMTiles reader"
139101
return .raw(500, "Internal Server Error", rsp_headers, {_ in })
140102
}
141103

142-
opts.Logger?.debug("Read data from \(path) to \(next)")
143-
144-
if opts.UseFileDescriptor {
145-
146-
let read_len = Int(UInt64(next) - start)
147-
opts.Logger?.debug("Read \(read_len) bytes from \(path)")
148-
149-
guard let data = readData(from: fd!.rawValue, length: Int(read_len)) else {
150-
opts.Logger?.error("Failed to read to \(next) for \(path)")
151-
rsp_headers["X-Error"] = "Failed to read from Protomaps tile"
152-
return .raw(500, "Internal Server Error", rsp_headers, {_ in })
104+
defer {
105+
if case .failure(let error) = pmtiles_reader.Close() {
106+
opts.Logger?.error("Failed to close PMTiles reader \(error)")
153107
}
154-
108+
}
109+
110+
opts.Logger?.debug("Read data from \(db_url.absoluteString) start: \(start) stop: \(stop)")
111+
112+
let body: Data!
113+
114+
let read_rsp = pmtiles_reader.Read(from: start, to: stop)
115+
116+
switch read_rsp {
117+
case .success(let data):
155118
body = data
156-
157-
} else {
158-
159-
do {
160-
body = try fh?.read(upToCount: next)
161-
} catch (let error){
162-
opts.Logger?.error("Failed to read to \(next) for \(path), \(error)")
163-
rsp_headers["X-Error"] = "Failed to read from Protomaps tile"
164-
return .raw(500, "Internal Server Error", rsp_headers, {_ in })
165-
}
166-
119+
case .failure(let error):
120+
opts.Logger?.error("Failed to read data from PMTiles reader, \(error)")
121+
rsp_headers["X-Error"] = "Failed to read from Protomaps tile"
122+
return .raw(500, "Internal Server Error", rsp_headers, {_ in })
167123
}
168124

169125
// https://httpwg.org/specs/rfc7233.html#header.accept-ranges
170126

171127
var filesize = "*"
172128

173-
do {
174-
if opts.UseFileDescriptor {
175-
176-
let size = try fd!.seek(offset: 0, from: FileDescriptor.SeekOrigin.end)
177-
filesize = String(size)
178-
179-
opts.Logger?.debug("filesize \(filesize)")
180-
181-
} else {
182-
183-
let size = try fh!.seekToEnd()
184-
filesize = String(size)
185-
}
186-
} catch (let error){
187-
opts.Logger?.warning("Failed to determine filesize for \(path), \(error)")
129+
let size_rsp = pmtiles_reader.Size()
130+
131+
switch size_rsp {
132+
case .success(let sz):
133+
filesize = String(sz)
134+
case .failure(let error):
135+
opts.Logger?.warning("Failed to determine size from PMTiles reader \(error)")
188136
}
189137

190138
let length = UInt64(next) - start
@@ -198,6 +146,8 @@ public func ServeProtomapsTiles(_ opts: ServeProtomapsOptions) -> ((HttpRequest)
198146
rsp_headers["Content-Range"] = content_range
199147
rsp_headers["Accept-Ranges"] = "bytes"
200148

149+
opts.Logger?.debug("Return 206 \(content_range)")
150+
201151
return .raw(206, "Partial Content", rsp_headers, { writer in
202152

203153
do {
@@ -208,23 +158,3 @@ public func ServeProtomapsTiles(_ opts: ServeProtomapsOptions) -> ((HttpRequest)
208158
})
209159
}
210160
}
211-
212-
internal func readData(from fileDescriptor: Int32, length: Int) -> Data? {
213-
// Create a Data buffer of the desired length
214-
var data = Data(count: length)
215-
216-
// Read the data into the Data buffer
217-
let bytesRead = data.withUnsafeMutableBytes { buffer -> Int in
218-
guard let baseAddress = buffer.baseAddress else { return -1 }
219-
return read(fileDescriptor, baseAddress, length)
220-
}
221-
222-
// Handle errors or end-of-file
223-
guard bytesRead > 0 else {
224-
return nil // Return nil if no bytes were read
225-
}
226-
227-
// Resize the Data object to the actual number of bytes read
228-
data.removeSubrange(bytesRead..<data.count)
229-
return data
230-
}

0 commit comments

Comments
 (0)