-
Notifications
You must be signed in to change notification settings - Fork 0
/
SwifterProtomaps.swift
133 lines (104 loc) · 5.2 KB
/
SwifterProtomaps.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
import Swifter
import Foundation
import Logging
/// ServeProtomapsOptions defines runtime options for serving Protomaps tiles
public struct ServeProtomapsOptions {
/// Root is the root directory to serve Protomaps tiles from
public var Root: URL
/// AllowOrigin is a string containing zero or more allowed origins for CORs requests and responses.
public var AllowOrigins: String
/// AllowOrigin is a string containing zero or more allowed headers for CORs requests and responses.
public var AllowHeaders: String
/// Logger is an option swift-logging instance for recording errors and warning.
public var Logger: Logger?
public init(root: URL) {
Root = root
AllowOrigins = ""
AllowHeaders = ""
}
}
/// ServeProtomapsTiles will serve HTTP range requests for zero or more Protomaps tile databases in a directory.
@available(iOS 13.4, *)
@available(macOS 10.15.4, *)
public func ServeProtomapsTiles(_ opts: ServeProtomapsOptions) -> ((HttpRequest) -> HttpResponse) {
return { r in
var rsp_headers = [String: String]()
guard let rel_path = r.params.first else {
return .raw(404, "Not found", rsp_headers, {_ in })
}
let uri = opts.Root.appendingPathComponent(rel_path.value)
let path = uri.absoluteString
// https://developer.apple.com/documentation/foundation/filehandle
guard let file = FileHandle(forReadingAtPath: path) else {
return .raw(404, "Not found", rsp_headers, {_ in })
}
defer {
do {
try file.close()
} catch (let error) {
opts.Logger?.warning("Failed to close \(path), \(error)")
}
}
guard var range_h = r.headers["range"] else {
rsp_headers["Access-Control-Allow-Origin"] = opts.AllowOrigins
rsp_headers["Access-Control-Allow-Headers"] = opts.AllowHeaders
return .raw(200, "OK", rsp_headers, {_ in })
}
let pat = "bytes=(\\d+)-(\\d+)"
guard let _ = range_h.range(of: pat, options: .regularExpression) else {
rsp_headers["X-Error"] = "Invalid or unsupported range request"
return .raw(400, "Bad Request", rsp_headers, {_ in })
}
range_h = range_h.replacingOccurrences(of: "bytes=", with: "")
let positions = range_h.split(separator: "-")
if positions.count != 2 {
rsp_headers["X-Error"] = "Invalid count for range request"
return .raw(400, "Bad Request", rsp_headers, {_ in })
}
guard let start = UInt64(positions[0]) else {
rsp_headers["X-Error"] = "Invalid starting range"
return .raw(400, "Bad Request", rsp_headers, {_ in })
}
guard let stop = Int(positions[1]) else {
rsp_headers["X-Error"] = "Invalid stopping range"
return .raw(400, "Bad Request", rsp_headers, {_ in })
}
if start > stop {
rsp_headers["X-Error"] = "Invalid range: Start value greater than stop value"
return .raw(400, "Bad Request", rsp_headers, {_ in })
}
let next = stop + 1
let body: Data!
file.seek(toFileOffset: start)
do {
body = try file.read(upToCount: next)
} catch (let error){
opts.Logger?.error("Failed to read to \(next) for \(path), \(error)")
rsp_headers["X-Error"] = "Failed to read from Protomaps tile"
return .raw(500, "Internal Server Error", rsp_headers, {_ in })
}
// https://httpwg.org/specs/rfc7233.html#header.accept-ranges
var filesize = "*"
do {
let size = try file.seekToEnd()
filesize = String(size)
} catch (let error){
opts.Logger?.warning("Failed to determine filesize for \(path), \(error)")
}
let length = UInt64(next) - start
let content_length = String(length)
let content_range = "bytes \(start)-\(next)/\(filesize)"
rsp_headers["Access-Control-Allow-Origin"] = opts.AllowOrigins
rsp_headers["Access-Control-Allow-Headers"] = opts.AllowHeaders
rsp_headers["Content-Length"] = content_length
rsp_headers["Content-Range"] = content_range
rsp_headers["Accept-Ranges"] = "bytes"
return .raw(206, "Partial Content", rsp_headers, { writer in
do {
try writer.write(body)
} catch (let error) {
opts.Logger?.error("Failed to write body, \(error)")
}
})
}
}