- Enable Apache2 on your mac server,
nano /etc/apache2/httpd.conf
.
DocumentRoot "/Library/WebServer/Documents"
<Directory "/Library/WebServer/Documents">
# add Indexes
Options FollowSymLinks Multiviews Indexes
MultiviewsMatch Any
AllowOverride None
Require all granted
</Directory>
# Uncommit this module
LoadModule autoindex_module libexec/apache2/mod_autoindex.so
then execute sudo apachectl restart
restart Apache2.
2. Download Container
from Xcode. it shoud be save as com.xxxx.xcappdata
.
3. Add/Edit/Remove file in com.xxxx.xcappdata/AppData/Documents/
(Most ussage)
4. Create client-upload-app-documents.sh
and app-documents-sync.sh
at same folder, replace those variable with your setting:
client-upload-app-documents.sh
##!/bin/bash
set -x
REMOTE_USER=YourServerUser
REMOTE_FILE_PATH=/Users/$REMOTE_USER/Downloads
REMOTE_HOST=YourServerIPorDomain(like 192.168.1.100 or file.yoursite.com)
function updateFiles() {
local SourcePath=$1
local DestinationPath=$2
local ScriptPath=$3
local AppName=$4
local SudoPassword=$5
local Random_String=$(openssl rand -hex 4)
local Remote_Source_Path=$REMOTE_FILE_PATH/tmp_$Random_String
scp $ScriptPath $REMOTE_USER@$REMOTE_HOST:$DestinationPath
scp -r $SourcePath/* $REMOTE_USER@$REMOTE_HOST:$Remote_Source_Path
ssh $REMOTE_USER@$REMOTE_HOST "cd $DestinationPath; chmod +x $ScriptPath; ./$ScriptPath -d $Remote_Source_Path -a $AppName -p $SudoPassword; rm -rf $Remote_Source_Path"
}
# command line arguments
# -d [source path]
# -a [app name]
# -s [script path]
# -p [sudo password]
while getopts d:a:s:p: option
do
case "${option}"
in
d) sourcePath=${OPTARG};;
a) appName=${OPTARG};;
s) scriptPath=${OPTARG};;
p) sudoPassword=${OPTARG};;
esac
done
if [ -z "$sudoPassword" ]; then
echo "sudo password is empty, exiting..."
exit 1
fi
if [ -z "$sourcePath" ]; then
echo "Source path is empty, exiting..."
exit 1
fi
if [ -z "$appName" ]; then
echo "App name is empty, exiting..."
exit 1
fi
if [ -z "$scriptPath" ]; then
echo "Script path is empty, exiting..."
exit 1
fi
updateFiles $sourcePath $REMOTE_FILE_PATH $scriptPath $appName $sudoPassword
app-documents-sync.sh
#!/bin/bash
set -x
AppPath=/Library/WebServer/Documents/App
function copyFilesToDocuments() {
local SourcePath=$1
local AppName=$2
local App_Documents_Path=$AppPath/$AppName/Documents
if [ -z "$SourcePath" ]; then
echo "Source path is empty, exiting..."
exit 1
fi
if [ ! -d "$App_Documents_Path" ] && [ ! -z "$AppName" ]; then
echo "App directory for app $AppName does not exist, creating it now..."
sudo mkdir -p $App_Documents_Path
# sudo chown -R _www:_www $App_Documents_Path
# sudo chmod -R 755 $App_Documents_Path
fi
echo "Copying files in $SourcePath to $App_Documents_Path... (Not top level folder)"
sudo cp -R $SourcePath/* $App_Documents_Path
echo "Done."
}
# command line arguments
# -d [source path]
# -a [app name]
# -p [sudo password]
while getopts d:a:p: option
do
case "${option}"
in
d) sourcePath=${OPTARG};;
a) appName=${OPTARG};;
p) sudoPassword=${OPTARG};;
esac
done
if [ ! -z "$sudoPassword" ]; then
echo $sudoPassword | sudo -S -v
fi
if [ -z "$sourcePath" ]; then
echo "Source path is empty, exiting..."
exit 1
fi
if [ -z "$appName" ]; then
echo "App name is empty, exiting..."
exit 1
fi
if [ ! -d "$AppPath" ]; then
echo "Documents directory does not exist, creating it now..."
sudo mkdir -p $AppPath
sudo chown -R _www:_www $AppPath
sudo chmod -R 755 $AppPath
fi
copyFilesToDocuments $sourcePath $appName
- Excute
client-upload-app-documents.sh
, upload or mvDocuments
files under Apache default root path/Library/WebServer/Documents/App/YourAppName/Documents
, you can name anything toYourAppName
,YourServerPassword
for sudo command mv file to/Library/WebServer/Documents
:
/path/to/client-upload-app-documents.sh -d /path/to/com.xxxx.xcappdata/AppData/Documents -a YourAppName -s app-documents-sync.sh -p YourServerPassword
check it by browsing http://YourServerIPorDomain:8080/App/YourAppName/Documents
, it shuld be no error.
- Now, back to your App project, Add
ContainerMixer.swift
:
import Foundation
import Logging
internal let logger = Logger(label: "com.container.mixer")
public protocol FileProvider {
func files(in path: String) async throws -> [URL]
func baseURL() throws -> URL
}
public enum FileDomain {
case documents
var folder: String {
switch self {
case .documents:
return "Documents"
}
}
var directory: FileManager.SearchPathDirectory {
switch self {
case .documents:
return .documentDirectory
}
}
}
public struct ContainerMixer {
private let provider: FileProvider
public var domain = FileDomain.documents
public init(_ provider: FileProvider) {
self.provider = provider
}
public func download() async throws -> [URL] {
let folder = domain.folder
let files = try await provider.files(in: folder)
var temp = [URL]()
for file in files {
temp += try await processFileURL(file)
}
return temp
}
func processFileURL(_ file: URL) async throws -> [URL] {
var temp = [URL]()
if file.absoluteString.hasSuffix("/") {
let next = try await provider.files(in: file.path)
for item in next {
let urls = try await processFileURL(item)
temp += urls
}
} else {
let url = try await reloadFile(file)
temp.append(url)
}
return temp
}
func reloadFile(_ file: URL) async throws -> URL {
let base = try provider.baseURL()
let relativePath = file.path
guard let request = URL(string: relativePath, relativeTo: base) else {
throw URLError(.badURL, userInfo: [NSLocalizedDescriptionKey: "\(base)\(relativePath)"])
}
let (url, _) = try await URLSession.shared.download(from: URLRequest(url: request))
let destination = try FileManager.default.url(for: domain.directory, in: .userDomainMask, appropriateFor: nil, create: false)
let subDirectory = relativePath.hasPrefix(destination.lastPathComponent) ? destination.deletingLastPathComponent():destination
let next = URL(fileURLWithPath: relativePath, relativeTo: subDirectory)
if FileManager.default.fileExists(atPath: next.path) {
logger.info("Removing \(next.path)")
try FileManager.default.removeItem(at: next)
} else {
let parent = next.deletingLastPathComponent()
if !FileManager.default.fileExists(atPath: parent.path) {
try FileManager.default.createDirectory(at: parent, withIntermediateDirectories: true)
logger.info("Creating \(parent.path)")
}
}
try FileManager.default.moveItem(at: url, to: next)
logger.info("Moving \(request.absoluteString) to \(next.path)")
return next
}
}
- Add
DocumentsSwap.swift
:
import Foundation
struct DocumentsSwap: FileProvider {
func start() async {
let mixer = ContainerMixer(self)
await Task.log {
let urls = try await mixer.download()
let description = urls.map {
"local file at \($0.path)"
}.joined(separator: "\n")
logger.info("downloaded and replaced \(urls.count) files\n\(description)")
}.value
}
func files(in path: String) async throws -> [URL] {
let base = try baseURL()
let string = base.absoluteString + "\(path)?F=0"
guard let url = URL(string: string) else {
throw URLError(.badURL, userInfo: [NSLocalizedDescriptionKey: string])
}
let request = URLRequest(url: url)
let (fileURL, _) = try await URLSession.shared.download(from: request)
let raw = try String(contentsOf: fileURL)
if #available(iOS 16.0, macOS 13.0, *) {
let regx = try Regex("<a\\s+href=\"([^\"\\?]+)\"\\s*>")
let strings = raw.matches(of: regx)
.compactMap { $0[1].substring }
let relativeURL: [URL] = strings
.compactMap {
if $0.hasPrefix("/") {
logger.info("skip \($0), because it is parent path for \(path)")
return nil
}
let charactor = path.hasSuffix("/") || $0.hasPrefix("/") ? "":"/"
return URL(string: "\(path)\(charactor)\($0)")
}
logger.info("found \(relativeURL.count) files in \(path)")
return relativeURL
} else {
// Fallback on earlier versions
}
return []
}
func baseURL() throws -> URL {
let host = URL(string: "http://YourServerIPorDomain:8080")!
let string = host.absoluteString + "/App/YourAppName/"
guard let url = URL(string: string) else {
throw URLError(.badURL, userInfo: [NSLocalizedDescriptionKey: string])
}
return url
}
}
- Edit
YourApp.swift
:
@main
struct YourApp: App {
@State var contentID = 1
var body: some Scene {
WindowGroup {
ContentView()
#if DEBUG
.id(contentID)
.task {
await DocumentsSwap().start()
contentID += 1
}
#endif
}
}
}
App will automatic download Documents
files from our server and replace it at app lanuch.
Please!!! Apple need fix this bug ASAP.