-
Notifications
You must be signed in to change notification settings - Fork 6
Expand file tree
/
Copy pathNavigatorDemoDocument.swift
More file actions
180 lines (138 loc) · 5.23 KB
/
NavigatorDemoDocument.swift
File metadata and controls
180 lines (138 loc) · 5.23 KB
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
//
// NavigatorDemoDocument.swift
// Shared
//
// Created by Manuel M T Chakravarty on 16/05/2022.
//
// The persistent representation of our documents is a nested folder structure containing text files and possibly also
// a file map.
import Observation
import SwiftUI
import UniformTypeIdentifiers
import os
import Synchronization
import Files
private let logger = Logger(subsystem: "org.justtesting.NavigatorDemo", category: "NavigatorDemoDocument")
private let fileMapName = ".FileMap.plist"
extension UTType {
static let textBundle: UTType = UTType(exportedAs: "org.justtesting.text-bundle")
}
// MARK: -
// MARK: Payload
struct Payload: FileContents {
/// Text in a file if it's a text file containing UTF-8.
///
var text: String? {
didSet {
backingData = nil
}
}
/// Data in a file unless `text` has been updated and not yet marshalled back to `Data` again.
///
private var backingData: Data?
init(text: String) {
self.text = text
}
/// Files ending with a text file extension are converted to text if they conform to UTF-8.
///
init(name: String, data: Data) throws {
self.backingData = data
if UTType(filenameExtension: (name as NSString).pathExtension, conformingTo: .text) != nil,
let text = String(data: data, encoding: .utf8) {
self.text = text
}
}
func data() throws -> Data {
if let data = backingData { return data }
else if let data = text?.data(using: .utf8) { return data }
else { throw CocoaError(.formatting) }
}
/// Update backing data if necessary.
///
mutating func flush() {
if backingData == nil,
let data = text?.data(using: .utf8)
{
backingData = data
}
}
}
// MARK: -
// MARK: Document
// NB: This is not nice! However, it seems to be the only way to get the setter on `NavigatorDemoDocument.texts` to
// compile without errors. Outside of `NavigatorDemoDocument`, we do restrict the use of file trees to MainActor.
// As a result, there are no races on file trees.
extension FileTree<Payload>: @unchecked @retroactive Sendable { }
@Observable
final class NavigatorDemoDocument: ReferenceFileDocument {
/// Snapshot type for `ReferenceFileDocument`.
///
struct Snapshot {
let texts: FullFileOrFolder<Payload>
let fileIDMap: FileIDMap
}
private let textsStorage: Mutex<FileTree<Payload>>
@MainActor
var texts: FileTree<Payload> {
get { textsStorage.withLock { $0 } }
set { textsStorage.withLock { $0 = newValue } }
}
static var readableContentTypes: [UTType] { [.textBundle] }
init() {
self.textsStorage = .init(FileTree(files: FullFileOrFolder(folder: FullFolder(children: [:]))))
}
init(text: String) {
let folder = FullFolder(children: ["MyText.txt": FileOrFolder(file: File(contents: Payload(text: text)))])
self.textsStorage = .init(FileTree(files: FullFileOrFolder(folder: folder)))
}
init(configuration: ReadConfiguration) throws {
guard configuration.file.isDirectory,
let fileWrappers = configuration.file.fileWrappers
else {
logger.error("Couldn't get directory file wrapper")
throw CocoaError(.fileReadCorruptFile)
}
// Get the persistent file ids if available.
let fileMap: FileIDMap?
if let fileMapFileWrapper = fileWrappers[fileMapName],
fileMapFileWrapper.isRegularFile,
let fileMapData = fileMapFileWrapper.regularFileContents
{
let decoder = PropertyListDecoder()
fileMap = try? decoder.decode(FileIDMap.self, from: fileMapData)
} else { fileMap = nil }
// Slurp in the tree of folders.
let folder = try FullFolder<Payload>(fileWrappers: fileWrappers, persistentIDMap: fileMap)
self.textsStorage = .init(FileTree(files: FullFileOrFolder(folder: folder)))
}
func snapshot(contentType: UTType) throws -> Snapshot {
// TODO: On iOS, we don't get passed the correct declared content type for some reason
if contentType != .textBundle {
logger.error("Snapshot of unknown content type: identifier = '\(contentType.identifier)'")
}
let snapshot = try textsStorage.withLock { texts in
Snapshot(texts: try texts.snapshot(), fileIDMap: texts.fileIDMap)
}
return snapshot
}
// NB: This is safe as we access everything through the snapshot and all directory file wrappers (which are mutable) are
// freshly created for this snapshot (i.e., not shared with the document model). Hence, updating the file map will
// not race.
func fileWrapper(snapshot: Snapshot, configuration: WriteConfiguration) throws -> FileWrapper {
let fileWrapper = try snapshot.texts.fileWrapper()
if fileWrapper.isDirectory {
let encoder = PropertyListEncoder()
encoder.outputFormat = .xml
let newFileMapData = try encoder.encode(snapshot.fileIDMap)
if let fileMapWrapper = fileWrapper.fileWrappers?[fileMapName] {
// If the file map wrapper is already up to date, don't make any further changes
if fileMapWrapper.isRegularFile && fileMapWrapper.regularFileContents == newFileMapData {
return fileWrapper
}
fileWrapper.removeFileWrapper(fileMapWrapper)
}
fileWrapper.addRegularFile(withContents: newFileMapData, preferredFilename: fileMapName)
}
return fileWrapper
}
}