forked from blinksh/blink
-
Notifications
You must be signed in to change notification settings - Fork 0
/
FileProviderEnumerator.swift
231 lines (201 loc) · 9.3 KB
/
FileProviderEnumerator.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
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
//////////////////////////////////////////////////////////////////////////////////
//
// B L I N K
//
// Copyright (C) 2016-2019 Blink Mobile Shell Project
//
// This file is part of Blink.
//
// Blink is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Blink is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Blink. If not, see <http://www.gnu.org/licenses/>.
//
// In addition, Blink is also subject to certain additional terms under
// GNU GPL version 3 section 7.
//
// You should have received a copy of these additional terms immediately
// following the terms and conditions of the GNU General Public License
// which accompanied the Blink Source Code. If not, see
// <http://www.github.com/blinksh/blink>.
//
////////////////////////////////////////////////////////////////////////////////
import BlinkFiles
import FileProvider
import Combine
import SSH
class FileProviderEnumerator: NSObject, NSFileProviderEnumerator {
let identifier: BlinkItemIdentifier
let translator: AnyPublisher<Translator, Error>
let cache: FileTranslatorCache
var cancellableBag: Set<AnyCancellable> = []
var currentAnchor: Int = 0
let log: BlinkLogger
init(enumeratedItemIdentifier: NSFileProviderItemIdentifier,
domain: NSFileProviderDomain,
cache: FileTranslatorCache) {
// TODO An enumerator may be requested for an open file, in order to enumerate changes to it.
if enumeratedItemIdentifier == .rootContainer {
self.identifier = BlinkItemIdentifier(domain.pathRelativeToDocumentStorage)
} else {
self.identifier = BlinkItemIdentifier(enumeratedItemIdentifier)
}
let path = self.identifier.path
self.cache = cache
self.log = BlinkLogger("enumeratorFor \(path)")
self.log.debug("Initialized")
self.translator = cache.rootTranslator(for: self.identifier)
.flatMap { t -> AnyPublisher<Translator, Error> in
path.isEmpty ? .just(t.clone()) : t.cloneWalkTo(path)
}.eraseToAnyPublisher()
// TODO Schedule an interval enumeration (pull) from the server.
super.init()
}
func invalidate() {
// TODO: perform invalidation of server connection if necessary?
// Stop the enumeration
self.log.debug("Invalidate")
cancellableBag = []
}
func enumerateItems(for observer: NSFileProviderEnumerationObserver, startingAt page: NSFileProviderPage) {
/*
- inspect the page to determine whether this is an initial or a follow-up request
If this is an enumerator for a directory, the root container or all directories:
- perform a server request to fetch directory contents
If this is an enumerator for the active set:
- perform a server request to update your local database
- fetch the active set from your local database
- inform the observer about the items returned by the server (possibly multiple times)
- inform the observer that you are finished with this page
*/
self.log.info("Enumeration requested")
// We use the local files and the representation of the remotes to construct the view of the system.
// It is a simpler way to warm up the local cache without having a persistent representation.
var containerTranslator: Translator!
translator
.flatMap { t -> AnyPublisher<FileAttributes, Error> in
containerTranslator = t
return t.stat()
}
.map { containerAttrs -> Translator in
// 1. Store the container reference
// TODO We may be able to skip this if stat would return '.'
if let reference = self.cache.reference(identifier: self.identifier) {
reference.updateAttributes(remote: containerAttrs)
} else {
let ref = BlinkItemReference(self.identifier,
remote: containerAttrs,
cache: self.cache)
self.cache.store(reference: ref)
}
return containerTranslator
}
.flatMap {
// 2. Stat both local and remote files.
// For remote, if the file is a link, then stat to know the real attributes
Publishers.Zip($0.isDirectory ? $0.directoryFilesAndAttributesResolvingLinks() : AnyPublisher($0.stat().collect()),
Local().walkTo(self.identifier.url.path)
.flatMap { $0.isDirectory ? $0.directoryFilesAndAttributes() : AnyPublisher($0.stat().collect()) }
.catch { _ in AnyPublisher.just([]) })
}
.map { (remoteFilesAttributes, localFilesAttributes) -> [BlinkItemReference] in
// 3.1 Collect all current file references
return remoteFilesAttributes.map { attrs -> BlinkItemReference in
// 3.2 Match local and remote files, and upsert accordingly
let fileIdentifier = BlinkItemIdentifier(parentItemIdentifier: self.identifier,
filename: attrs[.name] as! String)
// Find a local file that matches the remote.
let localAttrs = localFilesAttributes.first(where: { $0[.name] as! String == fileIdentifier.filename })
if let reference = self.cache.reference(identifier: fileIdentifier) {
reference.updateAttributes(remote: attrs, local: localAttrs)
return reference
} else {
let ref = BlinkItemReference(fileIdentifier,
remote: attrs,
local: localAttrs,
cache: self.cache)
// Store the reference in the internal DB for later usage.
self.cache.store(reference: ref)
return ref
}
}
}
.sink(
receiveCompletion: { completion in
switch completion {
case .failure(let error):
self.log.error("\(error)")
observer.finishEnumeratingWithError(error)
case .finished:
observer.finishEnumerating(upTo: nil)
}
},
receiveValue: {
self.log.info("Enumerated \($0.count) items")
observer.didEnumerate($0)
}).store(in: &cancellableBag)
}
func enumerateChanges(for observer: NSFileProviderChangeObserver, from anchor: NSFileProviderSyncAnchor) {
/* TODO:
- query the server for updates since the passed-in sync anchor
If this is an enumerator for the active set:
- note the changes in your local database
- inform the observer about item deletions and updates (modifications + insertions)
- inform the observer when you have finished enumerating up to a subsequent sync anchor
*/
// Schedule changes
let anchor = UInt(String(data: anchor.rawValue, encoding: .utf8)!)!
self.log.info("Enumerating changes at \(anchor) anchor")
guard let ref = self.cache.reference(identifier: self.identifier) else {
observer.finishEnumeratingWithError("Op not supported")
return
}
if let updatedItems = self.cache.updatedItems(container: self.identifier, since: anchor) {
// Atm only update changes, no deletion as we don't provide tombstone values.
self.log.info("\(updatedItems.count) items updated.")
observer.didUpdate(updatedItems)
} else if anchor < ref.syncAnchor {
observer.didUpdate([ref])
}
let newAnchor = ref.syncAnchor
let data = "\(newAnchor)".data(using: .utf8)
observer.finishEnumeratingChanges(upTo: NSFileProviderSyncAnchor(data!), moreComing: false)
}
/**
Request the current sync anchor.
To keep an enumeration updated, the system will typically
- request the current sync anchor (1)
- enumerate items starting with an initial page
- continue enumerating pages, each time from the page returned in the previous
enumeration, until finishEnumeratingUpToPage: is called with nextPage set to
nil
- enumerate changes starting from the sync anchor returned in (1)
- continue enumerating changes, each time from the sync anchor returned in the
previous enumeration, until finishEnumeratingChangesUpToSyncAnchor: is called
with moreComing:NO
This method will be called again if you signal that there are more changes with
-[NSFileProviderManager signalEnumeratorForContainerItemIdentifier:
completionHandler:] and again, the system will enumerate changes until
finishEnumeratingChangesUpToSyncAnchor: is called with moreComing:NO.
NOTE that the change-based observation methods are marked optional for historical
reasons, but are really required. System performance will be severely degraded if
they are not implemented.
*/
func currentSyncAnchor(completionHandler: @escaping (NSFileProviderSyncAnchor?) -> Void) {
guard let ref = self.cache.reference(identifier: self.identifier) else {
completionHandler(nil)
return
}
self.log.info("Requested anchor \(ref.syncAnchor)")
let data = "\(ref.syncAnchor)".data(using: .utf8)
completionHandler(NSFileProviderSyncAnchor(data!))
}
}