From de0b4270df8dd65d5a1992408191495d1f01dd00 Mon Sep 17 00:00:00 2001 From: Tommy van der Vorst Date: Wed, 31 Jul 2024 07:31:14 +0200 Subject: [PATCH] all: minimal set of changes for iOS app (#9619) ### Purpose This PR contains the set of changes needed to make Syncthing work on iOS for [my iOS app for Syncthing](https://github.com/pixelspark/sushitrain). Most changes originate from [the Mobius Sync fork](http://github.com/MobiusSync/syncthing/tree/ios). I have removed the changes from their fork that are not strictly needed for my app (i.e. their changes to the GUI and command line utilities, for instance) and squashed it all in a single commit. In summary, the changes are: * Resolve non-absolute paths to the 'Documents' folder (basically the only one an app can/should write user data to by default on iOS) * Tweaking of build flags/conditions for iOS (i.e. determine which basicfs_watch, ignoreresult variant to build for iOS) * Disable upgrade mechanism on iOS * Make `RequestGlobal` and `PullerProgress` public symbols * Expose syncthing.app's Model instance (app.M) * Add no-op stub for SetLowPriority on iOS I would very much appreciate these changes to be (eventually) merged to mainline syncthing, as this would allow my iOS app to track the mainline source code directly and removes the need (for me at least) for maintaining a separate fork. Perhaps the Mobius folks can also benefit from this (although as noted this branch does not contain their changes to e.g. the GUI). ### Testing This branch has been tested with the iOS app and appears to work fine. The full set of MobiusSync changes has been used before with success. ### Screenshots n/a ### Documentation There should be no visible changes for users due to this set of changes. --------- Co-authored-by: Simon Pickup --- AUTHORS | 2 + lib/fs/basicfs_watch.go | 4 +- lib/fs/basicfs_watch_eventtypes_darwin.go | 4 +- lib/fs/basicfs_watch_eventtypes_other.go | 4 +- lib/fs/basicfs_watch_unsupported.go | 4 +- .../ignoreresult/ignoreresult_foldcase.go | 2 +- .../ignoreresult/ignoreresult_nofoldcase.go | 2 +- lib/locations/locations.go | 4 +- lib/model/folder_sendrecv.go | 2 +- lib/model/folder_summary.go | 2 +- lib/model/mocks/model.go | 102 ++++++++++++++++++ lib/model/model.go | 8 +- lib/model/model_test.go | 2 +- lib/model/progressemitter.go | 4 +- lib/model/progressemitter_test.go | 2 +- lib/model/sharedpullerstate.go | 6 +- lib/osutil/lowprio_noop.go | 16 +++ lib/osutil/lowprio_unix.go | 4 +- lib/scanner/walk.go | 2 +- lib/syncthing/syncthing.go | 2 + lib/upgrade/upgrade_supported.go | 4 +- lib/upgrade/upgrade_unsupp.go | 4 +- 22 files changed, 156 insertions(+), 30 deletions(-) create mode 100644 lib/osutil/lowprio_noop.go diff --git a/AUTHORS b/AUTHORS index 533dc4096..a63609d71 100644 --- a/AUTHORS +++ b/AUTHORS @@ -305,6 +305,7 @@ Severin von Wnuck-Lipinski Shaarad Dalvi <60266155+shaaraddalvi@users.noreply.github.com> Simon Frei (imsodin) Simon Mwepu +Simon Pickup Sly_tom_cat Stefan Kuntz (Stefan-Code) Stefan Tatschner (rumpelsepp) @@ -325,6 +326,7 @@ Tobias Tom (tobiastom) Tom Jakubowski Tomasz WilczyƄski <5626656+tomasz1986@users.noreply.github.com> Tommy Thorn +Tommy van der Vorst Tully Robinson (tojrobinson) Tyler Brazier (tylerbrazier) Tyler Kropp diff --git a/lib/fs/basicfs_watch.go b/lib/fs/basicfs_watch.go index c65529cc5..ab275270a 100644 --- a/lib/fs/basicfs_watch.go +++ b/lib/fs/basicfs_watch.go @@ -4,10 +4,12 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this file, // You can obtain one at http://mozilla.org/MPL/2.0/. -//go:build !(solaris && !cgo) && !(darwin && !cgo) && !(android && amd64) +//go:build !(solaris && !cgo) && !(darwin && !cgo) && !(darwin && kqueue) && !(android && amd64) && !ios // +build !solaris cgo // +build !darwin cgo +// +build !darwin !kqueue // +build !android !amd64 +// +build !ios package fs diff --git a/lib/fs/basicfs_watch_eventtypes_darwin.go b/lib/fs/basicfs_watch_eventtypes_darwin.go index f7ca0e008..e8fa49a08 100644 --- a/lib/fs/basicfs_watch_eventtypes_darwin.go +++ b/lib/fs/basicfs_watch_eventtypes_darwin.go @@ -4,8 +4,8 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this file, // You can obtain one at http://mozilla.org/MPL/2.0/. -//go:build darwin && !kqueue && cgo -// +build darwin,!kqueue,cgo +//go:build darwin && !kqueue && cgo && !ios +// +build darwin,!kqueue,cgo,!ios package fs diff --git a/lib/fs/basicfs_watch_eventtypes_other.go b/lib/fs/basicfs_watch_eventtypes_other.go index beb38e7db..cf08856f7 100644 --- a/lib/fs/basicfs_watch_eventtypes_other.go +++ b/lib/fs/basicfs_watch_eventtypes_other.go @@ -4,8 +4,8 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this file, // You can obtain one at http://mozilla.org/MPL/2.0/. -//go:build !linux && !windows && !dragonfly && !freebsd && !netbsd && !openbsd && !solaris && !darwin && !cgo -// +build !linux,!windows,!dragonfly,!freebsd,!netbsd,!openbsd,!solaris,!darwin,!cgo +//go:build !linux && !windows && !dragonfly && !freebsd && !netbsd && !openbsd && !solaris && !darwin && !cgo && !ios +// +build !linux,!windows,!dragonfly,!freebsd,!netbsd,!openbsd,!solaris,!darwin,!cgo,!ios // Catch all platforms that are not specifically handled to use the generic // event types. diff --git a/lib/fs/basicfs_watch_unsupported.go b/lib/fs/basicfs_watch_unsupported.go index fa4ab80da..4d10dc71a 100644 --- a/lib/fs/basicfs_watch_unsupported.go +++ b/lib/fs/basicfs_watch_unsupported.go @@ -4,8 +4,8 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this file, // You can obtain one at http://mozilla.org/MPL/2.0/. -//go:build (solaris && !cgo) || (darwin && !cgo) || (android && amd64) -// +build solaris,!cgo darwin,!cgo android,amd64 +//go:build (solaris && !cgo) || (darwin && !cgo) || (android && amd64) || ios || (darwin && kqueue) +// +build solaris,!cgo darwin,!cgo android,amd64 ios darwin,kqueue package fs diff --git a/lib/ignore/ignoreresult/ignoreresult_foldcase.go b/lib/ignore/ignoreresult/ignoreresult_foldcase.go index a77f13329..ebb8d92a4 100644 --- a/lib/ignore/ignoreresult/ignoreresult_foldcase.go +++ b/lib/ignore/ignoreresult/ignoreresult_foldcase.go @@ -4,7 +4,7 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this file, // You can obtain one at https://mozilla.org/MPL/2.0/. -//go:build windows || darwin +//go:build windows || darwin || ios package ignoreresult diff --git a/lib/ignore/ignoreresult/ignoreresult_nofoldcase.go b/lib/ignore/ignoreresult/ignoreresult_nofoldcase.go index 55c7d23d5..714820e63 100644 --- a/lib/ignore/ignoreresult/ignoreresult_nofoldcase.go +++ b/lib/ignore/ignoreresult/ignoreresult_nofoldcase.go @@ -4,7 +4,7 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this file, // You can obtain one at https://mozilla.org/MPL/2.0/. -//go:build !windows && !darwin +//go:build !windows && !darwin && !ios package ignoreresult diff --git a/lib/locations/locations.go b/lib/locations/locations.go index ad92124a6..197c686eb 100644 --- a/lib/locations/locations.go +++ b/lib/locations/locations.go @@ -180,7 +180,7 @@ func defaultConfigDir(userHome string) string { case build.IsWindows: return windowsConfigDataDir() - case build.IsDarwin: + case build.IsDarwin, build.IsIOS: return darwinConfigDataDir(userHome) default: @@ -191,7 +191,7 @@ func defaultConfigDir(userHome string) string { // defaultDataDir returns the default data directory, where we store the // database, log files, etc. func defaultDataDir(userHome, configDir string) string { - if build.IsWindows || build.IsDarwin { + if build.IsWindows || build.IsDarwin || build.IsIOS { return configDir } diff --git a/lib/model/folder_sendrecv.go b/lib/model/folder_sendrecv.go index 2b77b67f0..7e8639af1 100644 --- a/lib/model/folder_sendrecv.go +++ b/lib/model/folder_sendrecv.go @@ -1579,7 +1579,7 @@ loop: activity.using(selected) var buf []byte blockNo := int(state.block.Offset / int64(state.file.BlockSize())) - buf, lastError = f.model.requestGlobal(f.ctx, selected.ID, f.folderID, state.file.Name, blockNo, state.block.Offset, int(state.block.Size), state.block.Hash, state.block.WeakHash, selected.FromTemporary) + buf, lastError = f.model.RequestGlobal(f.ctx, selected.ID, f.folderID, state.file.Name, blockNo, state.block.Offset, int(state.block.Size), state.block.Hash, state.block.WeakHash, selected.FromTemporary) activity.done(selected) if lastError != nil { l.Debugln("request:", f.folderID, state.file.Name, state.block.Offset, state.block.Size, selected.ID.Short(), "returned error:", lastError) diff --git a/lib/model/folder_summary.go b/lib/model/folder_summary.go index 95653c1de..b1d1e77d8 100644 --- a/lib/model/folder_summary.go +++ b/lib/model/folder_summary.go @@ -261,7 +261,7 @@ func (c *folderSummaryService) processUpdate(ev events.Event) { return case events.DownloadProgress: - data := ev.Data.(map[string]map[string]*pullerProgress) + data := ev.Data.(map[string]map[string]*PullerProgress) c.foldersMut.Lock() for folder := range data { c.folders[folder] = struct{}{} diff --git a/lib/model/mocks/model.go b/lib/model/mocks/model.go index 2049b6567..52c28df53 100644 --- a/lib/model/mocks/model.go +++ b/lib/model/mocks/model.go @@ -435,6 +435,28 @@ type Model struct { result1 protocol.RequestResponse result2 error } + RequestGlobalStub func(context.Context, protocol.DeviceID, string, string, int, int64, int, []byte, uint32, bool) ([]byte, error) + requestGlobalMutex sync.RWMutex + requestGlobalArgsForCall []struct { + arg1 context.Context + arg2 protocol.DeviceID + arg3 string + arg4 string + arg5 int + arg6 int64 + arg7 int + arg8 []byte + arg9 uint32 + arg10 bool + } + requestGlobalReturns struct { + result1 []byte + result2 error + } + requestGlobalReturnsOnCall map[int]struct { + result1 []byte + result2 error + } ResetFolderStub func(string) error resetFolderMutex sync.RWMutex resetFolderArgsForCall []struct { @@ -2558,6 +2580,84 @@ func (fake *Model) RequestReturnsOnCall(i int, result1 protocol.RequestResponse, }{result1, result2} } +func (fake *Model) RequestGlobal(arg1 context.Context, arg2 protocol.DeviceID, arg3 string, arg4 string, arg5 int, arg6 int64, arg7 int, arg8 []byte, arg9 uint32, arg10 bool) ([]byte, error) { + var arg8Copy []byte + if arg8 != nil { + arg8Copy = make([]byte, len(arg8)) + copy(arg8Copy, arg8) + } + fake.requestGlobalMutex.Lock() + ret, specificReturn := fake.requestGlobalReturnsOnCall[len(fake.requestGlobalArgsForCall)] + fake.requestGlobalArgsForCall = append(fake.requestGlobalArgsForCall, struct { + arg1 context.Context + arg2 protocol.DeviceID + arg3 string + arg4 string + arg5 int + arg6 int64 + arg7 int + arg8 []byte + arg9 uint32 + arg10 bool + }{arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8Copy, arg9, arg10}) + stub := fake.RequestGlobalStub + fakeReturns := fake.requestGlobalReturns + fake.recordInvocation("RequestGlobal", []interface{}{arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8Copy, arg9, arg10}) + fake.requestGlobalMutex.Unlock() + if stub != nil { + return stub(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *Model) RequestGlobalCallCount() int { + fake.requestGlobalMutex.RLock() + defer fake.requestGlobalMutex.RUnlock() + return len(fake.requestGlobalArgsForCall) +} + +func (fake *Model) RequestGlobalCalls(stub func(context.Context, protocol.DeviceID, string, string, int, int64, int, []byte, uint32, bool) ([]byte, error)) { + fake.requestGlobalMutex.Lock() + defer fake.requestGlobalMutex.Unlock() + fake.RequestGlobalStub = stub +} + +func (fake *Model) RequestGlobalArgsForCall(i int) (context.Context, protocol.DeviceID, string, string, int, int64, int, []byte, uint32, bool) { + fake.requestGlobalMutex.RLock() + defer fake.requestGlobalMutex.RUnlock() + argsForCall := fake.requestGlobalArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3, argsForCall.arg4, argsForCall.arg5, argsForCall.arg6, argsForCall.arg7, argsForCall.arg8, argsForCall.arg9, argsForCall.arg10 +} + +func (fake *Model) RequestGlobalReturns(result1 []byte, result2 error) { + fake.requestGlobalMutex.Lock() + defer fake.requestGlobalMutex.Unlock() + fake.RequestGlobalStub = nil + fake.requestGlobalReturns = struct { + result1 []byte + result2 error + }{result1, result2} +} + +func (fake *Model) RequestGlobalReturnsOnCall(i int, result1 []byte, result2 error) { + fake.requestGlobalMutex.Lock() + defer fake.requestGlobalMutex.Unlock() + fake.RequestGlobalStub = nil + if fake.requestGlobalReturnsOnCall == nil { + fake.requestGlobalReturnsOnCall = make(map[int]struct { + result1 []byte + result2 error + }) + } + fake.requestGlobalReturnsOnCall[i] = struct { + result1 []byte + result2 error + }{result1, result2} +} + func (fake *Model) ResetFolder(arg1 string) error { fake.resetFolderMutex.Lock() ret, specificReturn := fake.resetFolderReturnsOnCall[len(fake.resetFolderArgsForCall)] @@ -3258,6 +3358,8 @@ func (fake *Model) Invocations() map[string][][]interface{} { defer fake.remoteNeedFolderFilesMutex.RUnlock() fake.requestMutex.RLock() defer fake.requestMutex.RUnlock() + fake.requestGlobalMutex.RLock() + defer fake.requestGlobalMutex.RUnlock() fake.resetFolderMutex.RLock() defer fake.resetFolderMutex.RUnlock() fake.restoreFolderVersionsMutex.RLock() diff --git a/lib/model/model.go b/lib/model/model.go index f88118eb9..dab7b0575 100644 --- a/lib/model/model.go +++ b/lib/model/model.go @@ -117,6 +117,8 @@ type Model interface { DismissPendingFolder(device protocol.DeviceID, folder string) error GlobalDirectoryTree(folder, prefix string, levels int, dirsOnly bool) ([]*TreeEntry, error) + + RequestGlobal(ctx context.Context, deviceID protocol.DeviceID, folder, name string, blockNo int, offset int64, size int, hash []byte, weakHash uint32, fromTemporary bool) ([]byte, error) } type model struct { @@ -375,7 +377,7 @@ func (m *model) addAndStartFolderLockedWithIgnores(cfg config.FolderConfiguratio // it'll show up as errored later. if err := cfg.CreateRoot(); err != nil { - l.Warnln("Failed to create folder root directory", err) + l.Warnln("Failed to create folder root directory:", err) } else if err = cfg.CreateMarker(); err != nil { l.Warnln("Failed to create folder marker:", err) } @@ -2460,7 +2462,7 @@ func (m *model) deviceDidCloseRLocked(deviceID protocol.DeviceID, duration time. } } -func (m *model) requestGlobal(ctx context.Context, deviceID protocol.DeviceID, folder, name string, blockNo int, offset int64, size int, hash []byte, weakHash uint32, fromTemporary bool) ([]byte, error) { +func (m *model) RequestGlobal(ctx context.Context, deviceID protocol.DeviceID, folder, name string, blockNo int, offset int64, size int, hash []byte, weakHash uint32, fromTemporary bool) ([]byte, error) { conn, connOK := m.requestConnectionForDevice(deviceID) if !connOK { return nil, fmt.Errorf("requestGlobal: no connection to device: %s", deviceID.Short()) @@ -2566,7 +2568,7 @@ func (m *model) numHashers(folder string) int { return folderCfg.Hashers } - if build.IsWindows || build.IsDarwin || build.IsAndroid { + if build.IsWindows || build.IsDarwin || build.IsIOS || build.IsAndroid { // Interactive operating systems; don't load the system too heavily by // default. return 1 diff --git a/lib/model/model_test.go b/lib/model/model_test.go index 0032790af..60b25cd22 100644 --- a/lib/model/model_test.go +++ b/lib/model/model_test.go @@ -222,7 +222,7 @@ func BenchmarkRequestOut(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - data, err := m.requestGlobal(context.Background(), device1, "default", files[i%n].Name, 0, 0, 32, nil, 0, false) + data, err := m.RequestGlobal(context.Background(), device1, "default", files[i%n].Name, 0, 0, 32, nil, 0, false) if err != nil { b.Error(err) } diff --git a/lib/model/progressemitter.go b/lib/model/progressemitter.go index 537f6b561..86b56c1d7 100644 --- a/lib/model/progressemitter.go +++ b/lib/model/progressemitter.go @@ -118,12 +118,12 @@ func (t *ProgressEmitter) Serve(ctx context.Context) error { } func (t *ProgressEmitter) sendDownloadProgressEventLocked() { - output := make(map[string]map[string]*pullerProgress) + output := make(map[string]map[string]*PullerProgress) for folder, pullers := range t.registry { if len(pullers) == 0 { continue } - output[folder] = make(map[string]*pullerProgress) + output[folder] = make(map[string]*PullerProgress) for name, puller := range pullers { output[folder][name] = puller.Progress() } diff --git a/lib/model/progressemitter_test.go b/lib/model/progressemitter_test.go index 62d980f8c..bac8dff7a 100644 --- a/lib/model/progressemitter_test.go +++ b/lib/model/progressemitter_test.go @@ -39,7 +39,7 @@ func expectEvent(w events.Subscription, t *testing.T, size int) { if event.Type != events.DownloadProgress { t.Fatal("Unexpected event:", event, "at", caller(1)) } - data := event.Data.(map[string]map[string]*pullerProgress) + data := event.Data.(map[string]map[string]*PullerProgress) if len(data) != size { t.Fatal("Unexpected event data size:", data, "at", caller(1)) } diff --git a/lib/model/sharedpullerstate.go b/lib/model/sharedpullerstate.go index 6679909ba..384d05d6c 100644 --- a/lib/model/sharedpullerstate.go +++ b/lib/model/sharedpullerstate.go @@ -75,7 +75,7 @@ func newSharedPullerState(file protocol.FileInfo, fs fs.Filesystem, folderID, te } // A momentary state representing the progress of the puller -type pullerProgress struct { +type PullerProgress struct { Total int `json:"total"` Reused int `json:"reused"` CopiedFromOrigin int `json:"copiedFromOrigin"` @@ -405,13 +405,13 @@ func encryptionTrailerSize(file protocol.FileInfo) int64 { } // Progress returns the momentarily progress for the puller -func (s *sharedPullerState) Progress() *pullerProgress { +func (s *sharedPullerState) Progress() *PullerProgress { s.mut.RLock() defer s.mut.RUnlock() total := s.reused + s.copyTotal + s.pullTotal done := total - s.copyNeeded - s.pullNeeded file := len(s.file.Blocks) - return &pullerProgress{ + return &PullerProgress{ Total: total, Reused: s.reused, CopiedFromOrigin: s.copyOrigin, diff --git a/lib/osutil/lowprio_noop.go b/lib/osutil/lowprio_noop.go new file mode 100644 index 000000000..41f275744 --- /dev/null +++ b/lib/osutil/lowprio_noop.go @@ -0,0 +1,16 @@ +// Copyright (C) 2020 The Syncthing Authors. +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +//go:build ios +// +build ios + +package osutil + +// SetLowPriority not possible on some platforms +// I/O priority depending on the platform and OS. +func SetLowPriority() error { + return nil +} diff --git a/lib/osutil/lowprio_unix.go b/lib/osutil/lowprio_unix.go index d0885b861..deb7d2342 100644 --- a/lib/osutil/lowprio_unix.go +++ b/lib/osutil/lowprio_unix.go @@ -4,8 +4,8 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this file, // You can obtain one at https://mozilla.org/MPL/2.0/. -//go:build (!windows && !linux) || android -// +build !windows,!linux android +//go:build (!windows && !linux && !ios) || android +// +build !windows,!linux,!ios android package osutil diff --git a/lib/scanner/walk.go b/lib/scanner/walk.go index 2397444b4..3c34ca285 100644 --- a/lib/scanner/walk.go +++ b/lib/scanner/walk.go @@ -553,7 +553,7 @@ func (w *walker) walkSymlink(ctx context.Context, relPath string, info fs.FileIn // normalizePath returns the normalized relative path (possibly after fixing // it on disk), or skip is true. func (w *walker) normalizePath(path string, info fs.FileInfo) (normPath string, err error) { - if build.IsDarwin { + if build.IsDarwin || build.IsIOS { // Mac OS X file names should always be NFD normalized. normPath = norm.NFD.String(path) } else { diff --git a/lib/syncthing/syncthing.go b/lib/syncthing/syncthing.go index fd627b581..a97abba6b 100644 --- a/lib/syncthing/syncthing.go +++ b/lib/syncthing/syncthing.go @@ -77,6 +77,7 @@ type App struct { stopOnce sync.Once mainServiceCancel context.CancelFunc stopped chan struct{} + Model model.Model } func New(cfg config.Wrapper, dbBackend backend.Backend, evLogger events.Logger, cert tls.Certificate, opts Options) (*App, error) { @@ -249,6 +250,7 @@ func (a *App) startup() error { keyGen := protocol.NewKeyGenerator() m := model.NewModel(a.cfg, a.myID, a.ll, protectedFiles, a.evLogger, keyGen) + a.Model = m a.mainService.Add(m) diff --git a/lib/upgrade/upgrade_supported.go b/lib/upgrade/upgrade_supported.go index b43654d7e..35b5f540d 100644 --- a/lib/upgrade/upgrade_supported.go +++ b/lib/upgrade/upgrade_supported.go @@ -4,8 +4,8 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this file, // You can obtain one at https://mozilla.org/MPL/2.0/. -//go:build !noupgrade -// +build !noupgrade +//go:build !noupgrade && !ios +// +build !noupgrade,!ios package upgrade diff --git a/lib/upgrade/upgrade_unsupp.go b/lib/upgrade/upgrade_unsupp.go index b442024d2..f8d922279 100644 --- a/lib/upgrade/upgrade_unsupp.go +++ b/lib/upgrade/upgrade_unsupp.go @@ -4,8 +4,8 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this file, // You can obtain one at https://mozilla.org/MPL/2.0/. -//go:build noupgrade -// +build noupgrade +//go:build noupgrade || ios +// +build noupgrade ios package upgrade