Browse Source

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 <[email protected]>
Tommy van der Vorst 1 year ago
parent
commit
de0b4270df

+ 2 - 0
AUTHORS

@@ -305,6 +305,7 @@ Severin von Wnuck-Lipinski <[email protected]>
 Shaarad Dalvi <[email protected]> <[email protected]>
 Simon Frei (imsodin) <[email protected]>
 Simon Mwepu <[email protected]>
+Simon Pickup <[email protected]>
 Sly_tom_cat <[email protected]>
 Stefan Kuntz (Stefan-Code) <[email protected]> <[email protected]>
 Stefan Tatschner (rumpelsepp) <[email protected]> <[email protected]> <[email protected]>
@@ -325,6 +326,7 @@ Tobias Tom (tobiastom) <[email protected]>
 Tom Jakubowski <[email protected]>
 Tomasz Wilczyński <[email protected]> <[email protected]>
 Tommy Thorn <[email protected]>
+Tommy van der Vorst <[email protected]>
 Tully Robinson (tojrobinson) <[email protected]>
 Tyler Brazier (tylerbrazier) <[email protected]>
 Tyler Kropp <[email protected]>

+ 3 - 1
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
 

+ 2 - 2
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
 

+ 2 - 2
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.

+ 2 - 2
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
 

+ 1 - 1
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
 

+ 1 - 1
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
 

+ 2 - 2
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
 	}
 

+ 1 - 1
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)

+ 1 - 1
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{}{}

+ 102 - 0
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()

+ 5 - 3
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

+ 1 - 1
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)
 		}

+ 2 - 2
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()
 		}

+ 1 - 1
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))
 	}

+ 3 - 3
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,

+ 16 - 0
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
+}

+ 2 - 2
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
 

+ 1 - 1
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 {

+ 2 - 0
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)
 

+ 2 - 2
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
 

+ 2 - 2
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