feat: 优化云盘工作区与批量文件工具

This commit is contained in:
2026-07-16 12:37:26 +08:00
parent 3cfc1617c5
commit fc22f63db4
9 changed files with 1615 additions and 385 deletions
+265
View File
@@ -0,0 +1,265 @@
import Foundation
import SQLite3
struct CachedFileMetadata: Sendable {
let fileID: String
let isDirectory: Bool
let gcid: String?
let size: Int64?
let modifiedEpoch: Int64?
let subDirectoryCount: Int?
let subFileCount: Int?
var isUsable: Bool {
isDirectory ? (size != nil || subDirectoryCount != nil || subFileCount != nil) : !(gcid?.isEmpty ?? true)
}
func applying(to file: CloudFile) -> CloudFile {
var result = file
if let gcid, !gcid.isEmpty, result.gcid?.isEmpty != false { result.gcid = gcid }
if let size, result.size == nil || (result.isDirectory && result.size == 0) { result.size = size }
if let modifiedEpoch, result.modifiedAt.isEmpty { result.modifiedAt = Self.formatDate(modifiedEpoch) }
if let subDirectoryCount, result.subDirectoryCount == nil { result.subDirectoryCount = subDirectoryCount }
if let subFileCount, result.subFileCount == nil { result.subFileCount = subFileCount }
return result
}
var detailValue: JSONValue {
var fileInfo: [String: JSONValue] = ["fileId": .string(fileID)]
if let gcid { fileInfo["gcid"] = .string(gcid) }
if let modifiedEpoch { fileInfo["utime"] = .number(Double(modifiedEpoch)) }
var sizeInfo: [String: JSONValue] = [:]
if let size { sizeInfo["size"] = .number(Double(size)) }
if let subDirectoryCount { sizeInfo["subDirCount"] = .number(Double(subDirectoryCount)) }
if let subFileCount { sizeInfo["subFileCount"] = .number(Double(subFileCount)) }
return .object(["data": .object(["fileInfo": .object(fileInfo), "sizeInfo": .object(sizeInfo)]), "cache": .bool(true)])
}
private static func formatDate(_ epoch: Int64) -> String {
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "zh_CN")
formatter.dateFormat = "yyyy-MM-dd HH:mm"
return formatter.string(from: Date(timeIntervalSince1970: TimeInterval(epoch)))
}
}
actor FileMetadataCache {
static let shared = FileMetadataCache()
private var database: OpaquePointer?
private let transient = unsafeBitCast(-1, to: sqlite3_destructor_type.self)
private init() {
guard let baseURL = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first else { return }
let directory = baseURL.appendingPathComponent("com.example.guangya-mac", isDirectory: true)
do { try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true) }
catch { return }
let url = directory.appendingPathComponent("file-metadata.sqlite3")
guard sqlite3_open_v2(url.path, &database, SQLITE_OPEN_CREATE | SQLITE_OPEN_READWRITE | SQLITE_OPEN_FULLMUTEX, nil) == SQLITE_OK,
let database else { self.database = nil; return }
Self.execute(database, "PRAGMA journal_mode=WAL;")
Self.execute(database, "PRAGMA synchronous=NORMAL;")
Self.execute(database, """
CREATE TABLE IF NOT EXISTS file_index (
file_id TEXT PRIMARY KEY NOT NULL,
is_directory INTEGER NOT NULL DEFAULT 0,
gcid TEXT,
modified_epoch INTEGER,
directory_size INTEGER,
sub_directory_count INTEGER,
sub_file_count INTEGER,
cached_at REAL NOT NULL
);
""")
Self.execute(database, """
CREATE TABLE IF NOT EXISTS gcid_details (
gcid TEXT PRIMARY KEY NOT NULL,
size INTEGER,
cached_at REAL NOT NULL
);
""")
Self.execute(database, "CREATE INDEX IF NOT EXISTS idx_file_index_gcid ON file_index(gcid);")
Self.migrateLegacyTable(database)
}
deinit { if let database { sqlite3_close(database) } }
func metadata(for fileIDs: [String]) -> [String: CachedFileMetadata] {
guard let database, !fileIDs.isEmpty else { return [:] }
var statement: OpaquePointer?
let sql = """
SELECT i.file_id, i.is_directory, i.gcid,
CASE WHEN i.is_directory = 1 THEN i.directory_size ELSE g.size END,
i.modified_epoch, i.sub_directory_count, i.sub_file_count
FROM file_index AS i
LEFT JOIN gcid_details AS g ON g.gcid = i.gcid
WHERE i.file_id = ?1;
"""
guard sqlite3_prepare_v2(database, sql, -1, &statement, nil) == SQLITE_OK, let statement else { return [:] }
defer { sqlite3_finalize(statement) }
var result: [String: CachedFileMetadata] = [:]
for fileID in fileIDs {
sqlite3_reset(statement)
sqlite3_clear_bindings(statement)
sqlite3_bind_text(statement, 1, fileID, -1, transient)
guard sqlite3_step(statement) == SQLITE_ROW else { continue }
let value = Self.read(statement)
result[value.fileID] = value
}
return result
}
func metadata(for fileID: String) -> CachedFileMetadata? {
metadata(for: [fileID])[fileID]
}
func save(fileID: String, isDirectory: Bool, detail: JSONValue) {
let metadata = CachedFileMetadata(
fileID: fileID,
isDirectory: isDirectory,
gcid: detail.firstStringDeep(["gcid", "gcId", "gcidValue", "hash"]),
size: detail.firstInt64Deep(["size", "fileSize", "resSize", "totalSize", "dirSize", "folderSize"]),
modifiedEpoch: detail.firstInt64Deep(["utime", "ctime"]),
subDirectoryCount: detail.firstIntDeep(["subDirCount"]),
subFileCount: detail.firstIntDeep(["subFileCount"])
)
save(metadata)
}
func save(files: [CloudFile]) {
for file in files {
let hasGCID = !(file.gcid?.isEmpty ?? true)
let hasDirectoryCounts = file.subDirectoryCount != nil || file.subFileCount != nil
guard (!file.isDirectory && hasGCID) || (file.isDirectory && hasDirectoryCounts) else { continue }
save(CachedFileMetadata(
fileID: file.id,
isDirectory: file.isDirectory,
gcid: file.gcid,
size: file.size,
modifiedEpoch: nil,
subDirectoryCount: file.subDirectoryCount,
subFileCount: file.subFileCount
))
}
}
func save(_ metadata: CachedFileMetadata) {
guard let database else { return }
let existingGCID = metadata.isDirectory ? nil : self.metadata(for: metadata.fileID)?.gcid
let effectiveGCID = metadata.gcid?.isEmpty == false ? metadata.gcid : existingGCID
var indexStatement: OpaquePointer?
let sql = """
INSERT INTO file_index
(file_id, is_directory, gcid, modified_epoch, directory_size, sub_directory_count, sub_file_count, cached_at)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)
ON CONFLICT(file_id) DO UPDATE SET
is_directory = excluded.is_directory,
gcid = COALESCE(excluded.gcid, file_index.gcid),
modified_epoch = COALESCE(excluded.modified_epoch, file_index.modified_epoch),
directory_size = COALESCE(excluded.directory_size, file_index.directory_size),
sub_directory_count = COALESCE(excluded.sub_directory_count, file_index.sub_directory_count),
sub_file_count = COALESCE(excluded.sub_file_count, file_index.sub_file_count),
cached_at = excluded.cached_at;
"""
guard sqlite3_prepare_v2(database, sql, -1, &indexStatement, nil) == SQLITE_OK, let indexStatement else { return }
sqlite3_bind_text(indexStatement, 1, metadata.fileID, -1, transient)
sqlite3_bind_int(indexStatement, 2, metadata.isDirectory ? 1 : 0)
Self.bind(effectiveGCID, to: indexStatement, at: 3, transient: transient)
Self.bind(metadata.modifiedEpoch, to: indexStatement, at: 4)
Self.bind(metadata.isDirectory ? metadata.size : nil, to: indexStatement, at: 5)
Self.bind(metadata.subDirectoryCount.map(Int64.init), to: indexStatement, at: 6)
Self.bind(metadata.subFileCount.map(Int64.init), to: indexStatement, at: 7)
sqlite3_bind_double(indexStatement, 8, Date().timeIntervalSince1970)
sqlite3_step(indexStatement)
sqlite3_finalize(indexStatement)
guard !metadata.isDirectory, let effectiveGCID, !effectiveGCID.isEmpty else { return }
var detailStatement: OpaquePointer?
let detailSQL = """
INSERT INTO gcid_details (gcid, size, cached_at)
VALUES (?1, ?2, ?3)
ON CONFLICT(gcid) DO UPDATE SET
size = COALESCE(excluded.size, gcid_details.size),
cached_at = excluded.cached_at;
"""
guard sqlite3_prepare_v2(database, detailSQL, -1, &detailStatement, nil) == SQLITE_OK, let detailStatement else { return }
defer { sqlite3_finalize(detailStatement) }
sqlite3_bind_text(detailStatement, 1, effectiveGCID, -1, transient)
Self.bind(metadata.size, to: detailStatement, at: 2)
sqlite3_bind_double(detailStatement, 3, Date().timeIntervalSince1970)
sqlite3_step(detailStatement)
}
private static func read(_ statement: OpaquePointer) -> CachedFileMetadata {
CachedFileMetadata(
fileID: String(cString: sqlite3_column_text(statement, 0)),
isDirectory: sqlite3_column_int(statement, 1) != 0,
gcid: text(statement, column: 2),
size: integer(statement, column: 3),
modifiedEpoch: integer(statement, column: 4),
subDirectoryCount: integer(statement, column: 5).map(Int.init),
subFileCount: integer(statement, column: 6).map(Int.init)
)
}
private static func text(_ statement: OpaquePointer, column: Int32) -> String? {
guard sqlite3_column_type(statement, column) != SQLITE_NULL, let value = sqlite3_column_text(statement, column) else { return nil }
return String(cString: value)
}
private static func integer(_ statement: OpaquePointer, column: Int32) -> Int64? {
sqlite3_column_type(statement, column) == SQLITE_NULL ? nil : sqlite3_column_int64(statement, column)
}
private static func bind(_ value: String?, to statement: OpaquePointer, at index: Int32, transient: sqlite3_destructor_type) {
if let value { sqlite3_bind_text(statement, index, value, -1, transient) }
else { sqlite3_bind_null(statement, index) }
}
private static func bind(_ value: Int64?, to statement: OpaquePointer, at index: Int32) {
if let value { sqlite3_bind_int64(statement, index, value) }
else { sqlite3_bind_null(statement, index) }
}
@discardableResult
private static func execute(_ database: OpaquePointer, _ sql: String) -> Bool {
sqlite3_exec(database, sql, nil, nil, nil) == SQLITE_OK
}
private static func migrateLegacyTable(_ database: OpaquePointer) {
guard tableExists(database, name: "file_metadata") else { return }
execute(database, "BEGIN IMMEDIATE TRANSACTION;")
let mapped = execute(database, """
INSERT OR REPLACE INTO file_index
(file_id, is_directory, gcid, modified_epoch, directory_size, sub_directory_count, sub_file_count, cached_at)
SELECT file_id, is_directory, gcid, modified_epoch,
CASE WHEN is_directory = 1 THEN size ELSE NULL END,
sub_directory_count, sub_file_count, cached_at
FROM file_metadata;
""")
let detailed = execute(database, """
INSERT OR REPLACE INTO gcid_details (gcid, size, cached_at)
SELECT gcid, size, cached_at
FROM file_metadata
WHERE is_directory = 0 AND gcid IS NOT NULL AND gcid != '';
""")
guard mapped, detailed else {
execute(database, "ROLLBACK;")
return
}
guard execute(database, "DROP TABLE file_metadata;") else {
execute(database, "ROLLBACK;")
return
}
execute(database, "COMMIT;")
}
private static func tableExists(_ database: OpaquePointer, name: String) -> Bool {
var statement: OpaquePointer?
guard sqlite3_prepare_v2(database, "SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?1;", -1, &statement, nil) == SQLITE_OK,
let statement else { return false }
defer { sqlite3_finalize(statement) }
sqlite3_bind_text(statement, 1, name, -1, unsafeBitCast(-1, to: sqlite3_destructor_type.self))
return sqlite3_step(statement) == SQLITE_ROW
}
}