feat(dev mode): add AFC file explorer and dump profiles

This commit is contained in:
naturecodevoid
2023-04-09 13:38:44 -07:00
parent 423ac28ba3
commit 994b2318a9
5 changed files with 408 additions and 293 deletions

View File

@@ -9,6 +9,309 @@
import SwiftUI
import ZIPFoundation
import UniformTypeIdentifiers
import minimuxer
extension Binding<URL?>: Equatable {
public static func == (lhs: Binding<URL?>, rhs: Binding<URL?>) -> Bool {
return lhs.wrappedValue == rhs.wrappedValue
}
}
private protocol FileExplorerBackend {
func delete(_ path: URL) throws
func zip(_ path: URL) throws
func insert(file: URL, to: URL) throws
func iterate(_ directory: URL) -> DirectoryEntry
func getQuickLookURL(_ path: URL) throws -> URL
}
private class NormalFileExplorerBackend: FileExplorerBackend {
func delete(_ path: URL) throws {
try FileManager.default.removeItem(at: path)
}
func zip(_ path: URL) throws {
let dest = FileManager.default.documentsDirectory.appendingPathComponent(path.pathComponents.last! + ".zip")
do {
try FileManager.default.removeItem(at: dest)
} catch {}
try FileManager.default.zipItem(at: path, to: dest)
}
func insert(file: URL, to: URL) throws {
try FileManager.default.copyItem(at: file, to: to.appendingPathComponent(file.pathComponents.last!), shouldReplace: true)
}
private func _iterate(directory: URL, parent: URL) -> DirectoryEntry {
var directoryEntry = DirectoryEntry(path: directory, parent: parent, isFile: false)
if let contents = try? FileManager.default.contentsOfDirectory(at: directory, includingPropertiesForKeys: nil, options: []) {
for entry in contents {
if entry.hasDirectoryPath {
directoryEntry.children!.append(_iterate(directory: entry, parent: directory))
} else {
directoryEntry.children!.append(DirectoryEntry(path: entry, parent: directory, isFile: true, size: {
guard let attributes = try? FileManager.default.attributesOfItem(atPath: entry.description.replacingOccurrences(of: "file://", with: "")) else { return nil }
return attributes[FileAttributeKey.size] as? Double
}()))
}
}
}
return directoryEntry
}
func iterate(_ directory: URL) -> DirectoryEntry {
return _iterate(directory: directory, parent: directory)
}
func getQuickLookURL(_ path: URL) throws -> URL {
path
}
}
private class AfcFileExplorerBackend: FileExplorerBackend {
func delete(_ path: URL) throws {
try AfcFileManager.remove(path.description.replacingOccurrences(of: "file://", with: "").removingPercentEncoding!)
}
func zip(_ path: URL) throws {
throw NSError(domain: "AFC currently doesn't support zipping a directory/file. however, it is possible (we should be able to copy the files outside of AFC and then zip the copied directory/file), it just hasn't been implemented", code: -1)
}
func insert(file: URL, to: URL) throws {
let data = try Data(contentsOf: file)
let rustByteSlice = data.toRustByteSlice()
let to = to.appendingPathComponent(file.lastPathComponent).description.replacingOccurrences(of: "file://", with: "").removingPercentEncoding!
print("writing to \(to)")
try AfcFileManager.writeFile(to, rustByteSlice.forRust())
}
private func _addChildren(_ rustEntry: RustDirectoryEntryRef) -> DirectoryEntry {
var entry = DirectoryEntry(
path: URL(string: rustEntry.path().toString())!,
parent: URL(string: rustEntry.parent().toString())!,
isFile: rustEntry.isFile(),
size: rustEntry.size() != nil ? Double(rustEntry.size()!) : nil
)
for child in rustEntry.children() {
entry.children!.append(_addChildren(child))
}
return entry
}
func iterate(_ directory: URL) -> DirectoryEntry {
var directoryEntry = DirectoryEntry(path: directory, parent: directory, isFile: false)
for child in AfcFileManager.contents() {
directoryEntry.children!.append(_addChildren(child))
}
return directoryEntry
}
func getQuickLookURL(_ path: URL) throws -> URL {
throw NSError(domain: "AFC currently doesn't support viewing a file. however, it is possible (we should be able to copy the file outside of AFC and then view the copied file), it just hasn't been implemented", code: -1)
}
}
private struct DirectoryEntry: Identifiable {
var id = UUID()
var path: URL
var parent: URL
var isFile: Bool
var size: Double?
var children: [DirectoryEntry]? = []
var asString: String {
let str = path.description.replacingOccurrences(of: parent.description, with: "").removingPercentEncoding!
if str.count <= 0 {
return "/"
}
return str
}
}
private enum FileExplorerAction {
case delete
case zip
case insert
case quickLook
}
private struct File: View {
@ObservedObject private var iO = Inject.observer
var item: DirectoryEntry
var backend: FileExplorerBackend
@Binding var explorerHidden: Bool
@State var quickLookURL: URL?
@State var fileExplorerAction: FileExplorerAction?
@State var hidden = false
@State var isShowingFilePicker = false
@State var selectedFile: URL?
var body: some View {
AsyncFallibleButton(action: {
switch (fileExplorerAction) {
case .delete:
print("deleting \(item.path.description)")
try backend.delete(item.path)
case .zip:
print("zipping \(item.path.description)")
try backend.zip(item.path)
case .insert:
print("inserting \(selectedFile!.description) to \(item.path.description)")
try backend.insert(file: selectedFile!, to: item.path)
explorerHidden = true
explorerHidden = false
case .quickLook:
print("viewing \(item.path.description)")
quickLookURL = try backend.getQuickLookURL(item.path)
default:
print("unknown action for \(item.path.description): \(String(describing: fileExplorerAction))")
}
}, label: { execute in
HStack {
Text(item.asString)
if item.isFile {
Text(getFileSize(item.size)).foregroundColor(.secondary)
}
Spacer()
Menu {
if item.isFile {
SwiftUI.Button(action: {
fileExplorerAction = .quickLook
execute()
}) {
Label("View/Share", systemSymbol: .eye)
}
} else {
SwiftUI.Button(action: {
fileExplorerAction = .zip
execute()
}) {
Label("Save to ZIP file", systemSymbol: .squareAndArrowDown)
}
SwiftUI.Button {
isShowingFilePicker = true
} label: {
Label("Insert file", systemSymbol: .plus)
}
}
if item.asString != "/" {
SwiftUI.Button(action: {
fileExplorerAction = .delete
execute()
}) {
Label("Delete", systemSymbol: .trash)
}
}
} label: {
Image(systemSymbol: .ellipsis)
.frame(width: 20, height: 20) // Make it easier to tap
}
}
.onChange(of: $selectedFile) { file in
guard file.wrappedValue != nil else { return }
fileExplorerAction = .insert
execute()
}
}, afterFinish: { success in
switch (fileExplorerAction) {
case .delete:
if success { hidden = true }
case .zip:
UIApplication.shared.open(URL(string: "shareddocuments://" + FileManager.default.documentsDirectory.description.replacingOccurrences(of: "file://", with: ""))!, options: [:], completionHandler: nil)
default: break
}
}, wrapInButton: false)
.quickLookPreview($quickLookURL)
.sheet(isPresented: $isShowingFilePicker) {
DocumentPicker(selectedUrl: $selectedFile, supportedTypes: allUTITypes().map({ $0.identifier }))
.ignoresSafeArea()
}
.isHidden($hidden)
.enableInjection()
}
func getFileSize(_ bytes: Double?) -> String {
guard var bytes = bytes else { return "Unknown file size" }
// https://stackoverflow.com/a/14919494 (ported to swift)
let thresh = 1024.0;
if (bytes < thresh) {
return String(describing: bytes) + " B";
}
let units = ["kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
var u = -1;
while (bytes >= thresh && u < units.count - 1) {
bytes /= thresh;
u += 1;
}
return String(format: "%.2f", bytes) + " " + units[u];
}
}
struct FileExplorer: View {
@ObservedObject private var iO = Inject.observer
private var url: URL?
private var backend: FileExplorerBackend
private init(_ url: URL?, _ backend: FileExplorerBackend) {
self.url = url
self.backend = backend
}
static func normal(url: URL?) -> FileExplorer {
FileExplorer(url, NormalFileExplorerBackend())
}
static func afc() -> FileExplorer {
FileExplorer(URL(string: "/")!, AfcFileExplorerBackend())
}
@State var hidden = false
var body: some View {
List([backend.iterate(url!)], children: \.children) { item in
File(item: item, backend: backend, explorerHidden: $hidden)
}
.toolbar {
ToolbarItem {
SwiftUI.Button {
hidden = true
hidden = false
} label: {
Image(systemSymbol: .arrowClockwise)
}
}
}
.isHidden($hidden)
.enableInjection()
}
}
struct FileExplorer_Previews: PreviewProvider {
static var previews: some View {
FileExplorer.normal(url: FileManager.default.altstoreSharedDirectory)
}
}
// https://stackoverflow.com/a/72165424
func allUTITypes() -> [UTType] {
@@ -152,215 +455,3 @@ func allUTITypes() -> [UTType] {
return types + types_1 + types_2
}
extension Binding<URL?>: Equatable {
public static func == (lhs: Binding<URL?>, rhs: Binding<URL?>) -> Bool {
return lhs.wrappedValue == rhs.wrappedValue
}
}
private struct DirectoryEntry: Identifiable {
var id = UUID()
var path: URL
var parent: URL
var isFile = false
var childFiles = [URL]()
var childDirectories: [DirectoryEntry]?
var filesAndDirectories: [DirectoryEntry]? {
if childFiles.count <= 0 { return childDirectories }
var filesAndDirectories = childDirectories ?? []
for file in childFiles {
filesAndDirectories.insert(DirectoryEntry(path: file, parent: path, isFile: true), at: 0)
}
return filesAndDirectories.sorted(by: { $0.asString < $1.asString })
}
var asString: String {
let str = path.description.replacingOccurrences(of: parent.description, with: "").removingPercentEncoding!
if str.count <= 0 {
return "/"
}
return str
}
}
private enum FileExplorerAction {
case delete
case zip
case insert
}
private struct File: View {
@ObservedObject private var iO = Inject.observer
var item: DirectoryEntry
@Binding var explorerHidden: Bool
@State var quickLookURL: URL?
@State var fileExplorerAction: FileExplorerAction?
@State var hidden = false
@State var isShowingFilePicker = false
@State var selectedFile: URL?
var body: some View {
AsyncFallibleButton(action: {
switch (fileExplorerAction) {
case .delete:
print("deleting \(item.path.description)")
try FileManager.default.removeItem(at: item.path)
case .zip:
print("zipping \(item.path.description)")
let dest = FileManager.default.documentsDirectory.appendingPathComponent(item.path.pathComponents.last! + ".zip")
do {
try FileManager.default.removeItem(at: dest)
} catch {}
try FileManager.default.zipItem(at: item.path, to: dest)
case .insert:
print("inserting \(selectedFile!.description) to \(item.path.description)")
try FileManager.default.copyItem(at: selectedFile!, to: item.path.appendingPathComponent(selectedFile!.pathComponents.last!), shouldReplace: true)
explorerHidden = true
explorerHidden = false
default:
print("unknown action for \(item.path.description): \(String(describing: fileExplorerAction))")
}
}, label: { execute in
HStack {
Text(item.asString)
if item.isFile {
Text(getFileSize(file: item.path)).foregroundColor(.secondary)
}
Spacer()
Menu {
if item.isFile {
SwiftUI.Button(action: { quickLookURL = item.path }) {
Label("View/Share", systemSymbol: .eye)
}
} else {
SwiftUI.Button(action: {
fileExplorerAction = .zip
execute()
}) {
Label("Save to ZIP file", systemSymbol: .squareAndArrowDown)
}
SwiftUI.Button {
isShowingFilePicker = true
} label: {
Label("Insert file", systemSymbol: .plus)
}
}
if item.asString != "/" {
SwiftUI.Button(action: {
fileExplorerAction = .delete
execute()
}) {
Label("Delete", systemSymbol: .trash)
}
}
} label: {
Image(systemSymbol: .ellipsis)
.frame(width: 20, height: 20) // Make it easier to tap
}
}
.onChange(of: $selectedFile) { file in
guard file.wrappedValue != nil else { return }
fileExplorerAction = .insert
execute()
}
}, afterFinish: { success in
switch (fileExplorerAction) {
case .delete:
if success { hidden = true }
case .zip:
UIApplication.shared.open(URL(string: "shareddocuments://" + FileManager.default.documentsDirectory.description.replacingOccurrences(of: "file://", with: ""))!, options: [:], completionHandler: nil)
default: break
}
}, wrapInButton: false)
.quickLookPreview($quickLookURL)
.sheet(isPresented: $isShowingFilePicker) {
DocumentPicker(selectedUrl: $selectedFile, supportedTypes: allUTITypes().map({ $0.identifier }))
.ignoresSafeArea()
}
.isHidden($hidden)
.enableInjection()
}
func getFileSize(file: URL) -> String {
guard let attributes = try? FileManager.default.attributesOfItem(atPath: file.description.replacingOccurrences(of: "file://", with: "")) else { return "Unknown file size" }
var bytes = attributes[FileAttributeKey.size] as! Double
// https://stackoverflow.com/a/14919494 (ported to swift)
let thresh = 1024.0;
if (bytes < thresh) {
return String(describing: bytes) + " B";
}
let units = ["kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
var u = -1;
while (bytes >= thresh && u < units.count - 1) {
bytes /= thresh;
u += 1;
}
return String(format: "%.2f", bytes) + " " + units[u];
}
}
struct FileExplorer: View {
@ObservedObject private var iO = Inject.observer
var url: URL?
@State var hidden = false
var body: some View {
List([iterateOverDirectory(directory: url!, parent: url!)], children: \.filesAndDirectories) { item in
File(item: item, explorerHidden: $hidden)
}
.toolbar {
ToolbarItem {
SwiftUI.Button {
hidden = true
hidden = false
} label: {
Image(systemSymbol: .arrowClockwise)
}
}
}
.isHidden($hidden)
.enableInjection()
}
private func iterateOverDirectory(directory: URL, parent: URL) -> DirectoryEntry {
var directoryEntry = DirectoryEntry(path: directory, parent: parent)
if let contents = try? FileManager.default.contentsOfDirectory(at: directory, includingPropertiesForKeys: nil, options: []) {
for entry in contents {
if entry.hasDirectoryPath {
if directoryEntry.childDirectories == nil { directoryEntry.childDirectories = [] }
directoryEntry.childDirectories!.append(iterateOverDirectory(directory: entry, parent: directory))
} else {
directoryEntry.childFiles.append(entry)
}
}
}
return directoryEntry
}
}
struct FileExplorer_Previews: PreviewProvider {
static var previews: some View {
FileExplorer(url: FileManager.default.altstoreSharedDirectory)
}
}