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.
86 lines
2.7 KiB
Swift
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)
|
|
}
|
|
}
|