Files
Felitendo ff0d3108fe feat: merge the web dashboard into the monorepo as ftstore admin
FTStore now holds both halves of the product:

  ios/   the swift app — the fork of claration/Feather, unchanged apart
         from its new home
  web/   FTStore Admin, formerly its own repository. serves the
         dashboard at /admin, the ftstore.json feed and IPA downloads

app-repo.json and Images/ stay at the repo root on purpose: the former is
a published AltStore source whose raw.githubusercontent URL is what
installed copies resolve updates through, so moving it would break every
subscriber.

The admin server is renamed throughout: package ftstore-admin, images
ghcr.io/<owner>/ftstore-admin-{app,worker}, postgres role/db ftstore,
session cookie ftstore_session, temp dir /tmp/ftstore, backup type
ftstore-settings, outbound user-agent FTStoreAdmin. backups written under the old
name still import, and compose is pinned to name: ftstore so the
volume prefix does not follow the directory name.

apply-rebrand.sh gains a relocation step: upstream's tree is rooted at
the repo root while ours lives in ios/, so a merge drops Feather's files
next to ios/ and the script folds them in before renaming. it never
touches web/. still idempotent.

release tags are namespaced — v* for the app, web-v* for the server —
so a server release no longer triggers a macOS build or rewrites
app-repo.json. workflows are prefixed ios-/web- and path-filtered.
2026-08-03 10:25:03 +02:00

86 lines
2.7 KiB
Swift

//
// FileManager+bundle.swift
// Feather
//
// Created by samara on 16.04.2025.
//
import Foundation.NSFileManager
extension FileManager {
public func getPath(in directory: URL, for pathExtension: String) -> URL? {
guard let contents = try? contentsOfDirectory(at: directory, includingPropertiesForKeys: nil, options: [.skipsHiddenFiles]) else {
return nil
}
return contents.first(where: { $0.pathExtension == pathExtension })
}
public func isFileFromFileProvider(at url: URL) -> Bool {
if let resourceValues = try? url.resourceValues(forKeys: [.isUbiquitousItemKey, .fileResourceIdentifierKey]),
resourceValues.isUbiquitousItem == true {
return true
}
let path = url.path
if path.contains("/Library/CloudStorage/") || path.contains("/File Provider Storage/") {
return true
}
return false
}
public func removeFileIfNeeded(at url: URL) throws {
if self.fileExists(atPath: url.path) {
try self.removeItem(at: url)
}
}
public func moveFileIfNeeded(from sourceURL: URL, to destinationURL: URL) throws {
if !self.fileExists(atPath: destinationURL.path) {
try self.moveItem(at: sourceURL, to: destinationURL)
}
}
public func createDirectoryIfNeeded(at url: URL) throws {
if !self.fileExists(atPath: url.path) {
try self.createDirectory(at: url, withIntermediateDirectories: true, attributes: nil)
}
}
static public func forceWrite(content: String, to filename: String) throws {
let path = URL.documentsDirectory.appendingPathComponent(filename)
try content.write(to: path, atomically: true, encoding: .utf8)
}
public func decodeAndWrite(base64: String, pathComponent: String) -> URL? {
let raw = base64.replacingOccurrences(of: " ", with: "+")
guard let data = Data(base64Encoded: raw) else { return nil }
let dir = self.temporaryDirectory.appendingPathComponent(UUID().uuidString + pathComponent)
try? data.write(to: dir)
return dir
}
// FeatherTweak
public func moveAndStore(_ url: URL, with prepend: String, completion: @escaping (URL) -> Void) {
let destination = _getDestination(url, with: prepend)
try? createDirectoryIfNeeded(at: destination.temp)
try? self.copyItem(at: url, to: destination.dest)
completion(destination.dest)
}
public func deleteStored(_ url: URL, completion: @escaping (URL) -> Void) {
try? FileManager.default.removeItem(at: url)
completion(url)
}
// FeatherTweak
private func _getDestination(_ url: URL, with prepend: String) -> (temp: URL, dest: URL) {
let tempDir = self.temporaryDirectory.appendingPathComponent("\(prepend)_\(UUID().uuidString)", isDirectory: true)
let destinationUrl = tempDir.appendingPathComponent(url.lastPathComponent)
return (tempDir, destinationUrl)
}
}