-
Notifications
You must be signed in to change notification settings - Fork 3
/
NavigatorDemoDocument.swift
155 lines (117 loc) · 4.17 KB
/
NavigatorDemoDocument.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
//
// 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 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 its 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
@Observable
final class NavigatorDemoDocument: ReferenceFileDocument {
typealias Snapshot = FullFileOrFolder<Payload>
var texts: FileTree<Payload>
static var readableContentTypes: [UTType] { [.textBundle] }
init() {
self.texts = FileTree(files: FullFileOrFolder(folder: FullFolder(children: [:])))
}
init(text: String) {
let folder = FullFolder(children: ["MyText.txt": FileOrFolder(file: File(contents: Payload(text: text)))])
self.texts = 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)
texts = 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)'")
}
return try texts.snapshot()
}
func fileWrapper(snapshot: Snapshot, configuration: WriteConfiguration) throws -> FileWrapper {
let fileWrapper = try snapshot.fileWrapper()
if fileWrapper.isDirectory {
let encoder = PropertyListEncoder()
encoder.outputFormat = .xml
let newFileMapData = try encoder.encode(texts.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
}
}