Compare commits

...
Author SHA1 Message Date
Simon Frei ed6b56d48f chore(model): don't check existing file twice in rename detection (#10833)
This is related to and lessens issue:

https://github.com/syncthing/syncthing/issues/10777

@SoongVilda found that a recent change to track already deleted or
existing files only in the scope of a batch, also stopped tracking
already existing files. Thus the osutil.IsDeleted check can be executed
many times if lots of identical files get picked up in a scan:


https://github.com/syncthing/syncthing/issues/10777#issuecomment-4991039201

The change in question:

chore(model): more efficient tracking of renames during scan (#10653)
2721b7b522

---------

Signed-off-by: Simon Frei <freisim93@gmail.com>
2026-07-27 08:28:04 +02:00
Syncthing Release Automation f145da2613 chore(gui, man, authors): update docs, translations, and contributors 2026-07-27 04:56:41 +00:00
Jakob Borg 2f55d28b5c fix(api): handle empty path in static request (#10837)
The panic would be recovered by the HTTP server, but this is cleaner.

Signed-off-by: Jakob Borg <jakob@kastelo.net>
2026-07-26 18:54:45 +00:00
Jakob Borg 733f63592f fix(versioner): handle invalid empty command (#10836)
Same same

Signed-off-by: Jakob Borg <jakob@kastelo.net>
2026-07-26 18:51:53 +00:00
Jakob Borg b8a255cc96 fix(ignore): handle pattern resulting in empty string (#10835)
Minor fix to avoid a panic in ignore matching.

Signed-off-by: Jakob Borg <jakob@kastelo.net>
2026-07-26 20:50:14 +02:00
Jakob Borg 703b185982 fix(upnp): guard against out of index string access (#10834)
Broken responses may have empty path component.

Signed-off-by: Jakob Borg <jakob@kastelo.net>
2026-07-26 18:37:28 +00:00
Jakob Borg 119d5e72ef chore: deflake TestRecvOnlyRevertNeeds (#10827)
There was a rare, tricky race condition in the setup, where a pull could
get started, find the blocks from the file we injected in
updateLocalsFromScanning, copy them into a temp file, then fail the pull
because the file was already in place. Then later, after the revert,
another pull would start, find the temp file and reuse it, making it so
needed=0 instead of the expected needed>0 the test checks for.

This avoids all that by setting the local index data prior to getting it
from the "remote" device, thus avoiding the initial pull and temp file
creation etc.

Signed-off-by: Jakob Borg <jakob@kastelo.net>
2026-07-25 08:47:42 +00:00
tbodt 3993f62beb chore(gui): better describe when a pending folder/device notification will reappear (#10808)
The tooltip on the dismiss button for notifications confused me a bit
(wdym mean "may" recur?) so here is an attempt at a clearer message.

Signed-off-by: tbodt <tbodt@tbodt.com>
2026-07-25 10:01:32 +02:00
Rohit Tanwar 130850097d fix(gui): add bottom margin to folder action buttons for wrapped spacing (#10728)
adds a bottom margin to buttons inside panel footers, ensuring a
comfortable gap between wrapped button rows. (Fixes #10689 )

Signed-off-by: rohitanwar <mst10041967@gmail.com>
2026-07-25 09:54:46 +02:00
Jakob Borg ec35a95318 chore: slightly optimise rename detection (ref #10777) (#10819)
Only run rename detection for new files. This skips an expensive check
for all updates to existing files. The tradeoff is that we no longer
immediately detect renames on top of another file as a rename -- this
may instead become a copy+delete operation on the destination.

---------

Signed-off-by: Jakob Borg <jakob@kastelo.net>
2026-07-25 07:46:56 +00:00
Jakob Borg b15ff42d52 chore: remove ignore file caching entirely (#10813)
Effectively nobody uses it, it cost more than it was worth, the code
doesn't carry its weight. Remove it and let any similar mechanism in the
future be an internal implementation detail.

Closes #10812

Signed-off-by: Jakob Borg <jakob@kastelo.net>
2026-07-25 09:43:22 +02:00
Jakob Borg 2f81bf5d34 chore(fs): casefs keep first instead of last mapping
Closes #10823, closes #10825.

Signed-off-by: Jakob Borg <jakob@kastelo.net>
2026-07-25 09:36:39 +02:00
Jakob Borg 39b0bd20d0 chore: new code contribution guidelines (#10821)
Reflecting current challenges.

Signed-off-by: Jakob Borg <jakob@kastelo.net>
2026-07-25 09:33:38 +02:00
Jakob Borg 977f790143 build(deps): update dependencies (#10824)
Signed-off-by: Jakob Borg <jakob@kastelo.net>
2026-07-24 08:09:47 +02:00
tbodt 5fa3d9f418 fix(gui): fix expanding one folder not collapsing others in group (#10809)
Regression introduced after folder groups were added in
6a26d56ad9

Signed-off-by: tbodt <tbodt@tbodt.com>
2026-07-21 04:45:07 +00:00
Frederic d73c00799b chore(gui): lazy-render collapsed panels to reduce watcher count (#10772)
Adds a `lazyCollapse` directive that renders panel body content only
while expanded. Collapsed panels contribute zero watchers — content is
added to the DOM on expand (`show.bs.collapse`) and removed after the
collapse animation completes (`hidden.bs.collapse`).


Signed-off-by: Finomosec <1665799+Finomosec@users.noreply.github.com>
2026-07-20 21:03:53 +00:00
Jakob Borg 9f7b4d4104 fix(model): properly health-check up-to-date folders (fixes #10546) (#10773)
Previously we'd skip the health check for an up-to-date folder, so it
would continue looking up-to-date even when the folder path missing.

---------

Signed-off-by: Jakob Borg <jakob@kastelo.net>
2026-07-20 22:54:31 +02:00
Jakob Borg d710874903 chore: fix a couple of unclosed http connections (#10806)
Signed-off-by: Jakob Borg <jakob@kastelo.net>
2026-07-20 20:22:40 +00:00
Syncthing Release Automation 578cc5a165 chore(gui, man, authors): update docs, translations, and contributors 2026-07-20 04:56:12 +00:00
Andrew Gunnerson a0171e3a9b build: allow inotify on Android amd64 (ref #8710) (#10783)
ad0044fec8 previously disabled it due to
seccomp crashes. This was caused by `golang.org/x/sys`'s
`unix.EpollWait` function using the `epoll_wait` syscall instead of the
`epoll_pwait` syscall. bionic libc implements the `epoll_wait` C wrapper
function in terms of the `epoll_pwait` syscall, so this never affected C
code.

`golang.org/x/sys` switched to the `epoll_pwait` syscall in 2016, but
only for arm64. As of v0.47.0, it now uses `epoll_pwait` for all
architectures.

Upstream bug: https://github.com/golang/go/issues/80080
Upstream fix: https://go-review.googlesource.com/c/sys/+/792700
Upstream arm64 fix from 2016:
https://go-review.googlesource.com/c/sys/+/21971

---

With this change, Syncthing no longer crashes when inotify is allowed
and enabled in the Android emulator when run inside an app context. (The
seccomp policies do not affect execution under `adb shell`.)

This is the crash that occurs if inotify is enabled without the v0.47.0
update:
```
2026-07-08 10:59:36.711202 -0400 F/DEBUG   (16538): *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
2026-07-08 10:59:36.711219 -0400 F/DEBUG   (16538): Build fingerprint: 'google/sdk_gphone16k_x86_64/emu64xa16k:17/CE2A.260420.019/15611780:userdebug/dev-keys'
2026-07-08 10:59:36.711224 -0400 F/DEBUG   (16538): Kernel Release: '6.12.58-android16-6-gccafb60de224-ab14828483'
2026-07-08 10:59:36.711226 -0400 F/DEBUG   (16538): Revision: '0'
2026-07-08 10:59:36.711228 -0400 F/DEBUG   (16538): ABI: 'x86_64'
2026-07-08 10:59:36.711230 -0400 F/DEBUG   (16538): Timestamp: 2026-07-08 10:59:36.617348693-0400
2026-07-08 10:59:36.711232 -0400 F/DEBUG   (16538): Process uptime: 2s
2026-07-08 10:59:36.711234 -0400 F/DEBUG   (16538): Page size: 16384 bytes
2026-07-08 10:59:36.711236 -0400 F/DEBUG   (16538): Executable: /system/bin/app_process64
2026-07-08 10:59:36.711239 -0400 F/DEBUG   (16538): Cmdline: com.chiller3.basicsync.debug
2026-07-08 10:59:36.711241 -0400 F/DEBUG   (16538): pid: 16502, ppid: 436, tid: 16531, name: Thread-6  >>> com.chiller3.basicsync.debug <<<
2026-07-08 10:59:36.711243 -0400 F/DEBUG   (16538): uid: 10231
2026-07-08 10:59:36.711886 -0400 F/DEBUG   (16538): signal 31 (SIGSYS), code 1 (SYS_SECCOMP), fault addr --------
2026-07-08 10:59:36.711899 -0400 F/DEBUG   (16538): Cause: seccomp prevented call to disallowed x86_64 system call 232
2026-07-08 10:59:36.711908 -0400 F/DEBUG   (16538):     rax 00000000000000e8  rbx 0000000000000078  rcx 00007372d956826e  rdx 0000000000000001
2026-07-08 10:59:36.711912 -0400 F/DEBUG   (16538):     r8  0000000000000000  r9  0000000000000000  r10 ffffffffffffffff  r11 0000000000000206
2026-07-08 10:59:36.712123 -0400 F/DEBUG   (16538):     r12 0000737291cc5d00  r13 000025897adcad20  r14 000025897abc7c20  r15 ffffffffffffffff
2026-07-08 10:59:36.712134 -0400 F/DEBUG   (16538):     rdi 0000000000000078  rsi 000025897af9ef7c
2026-07-08 10:59:36.712137 -0400 F/DEBUG   (16538):     rbp 000025897af9ee28  rsp 000025897af9ede8  rip 00007372d956826e  err 0000000000000000
2026-07-08 10:59:36.712139 -0400 F/DEBUG   (16538): 1 total frames
2026-07-08 10:59:36.712141 -0400 F/DEBUG   (16538): backtrace:
2026-07-08 10:59:36.712145 -0400 F/DEBUG   (16538):       #00 pc 0000000000d2826e  /data/app/~~s99umF1bl81-VDLSDwy30g==/com.chiller3.basicsync.debug-5Lq8YsTwbMz_SXt-F_uHaQ==/base.apk (offset 0x2e4c000)
2026-07-08 10:59:36.712431 -0400 F/DEBUG   (16538): Note: To display stack pointer information, use the pbtombstone tool:
2026-07-08 10:59:36.712443 -0400 F/DEBUG   (16538):         pbtombstone --display-sp tombstone_XX.pb
```

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2026-07-16 16:08:55 +00:00
Jakob Borg 1722f0a7b2 fix(strelaypoolsrv): locking correctness (#10801)
Fixes a couple of races on relay handling in the pool server.

Fixes #10799.
Closes #10800.

Signed-off-by: Jakob Borg <jakob@kastelo.net>
2026-07-16 16:01:47 +00:00
Jakob Borg c904140b3c fix: avoid warning on does-not-exist scan error (fixes #10465) (#10791)
We never need to treat these as warnings from a scan.

Signed-off-by: Jakob Borg <jakob@kastelo.net>
2026-07-16 09:20:33 +08:00
Jakob Borg c1d8045d86 fix(ignore, fs): allow loading ignore patterns behind symlink (fixes #10785) (#10786)
The recent change to disallow following symlinks by default conflicts
with the expected behavior of .stignore. This adds a new flag which may
be passed to OpenFile to skip default symlink-forbidding open flag.

---------

Signed-off-by: Jakob Borg <jakob@kastelo.net>
2026-07-16 09:19:13 +08:00
Syncthing Release Automation d4cffd848e chore(gui, man, authors): update docs, translations, and contributors 2026-07-13 04:53:32 +00:00
Syncthing Release Automation 5277033906 chore(gui, man, authors): update docs, translations, and contributors 2026-07-06 05:07:17 +00:00
Jakob Borg ef4629ea56 chore(model): simplify FileInfoBatch size computation (#10776)
We used a size computation to generate reasonably sized batches, but the
ProtoSize call is fairly expensive as it requires a conversion to a wire
type etc. We don't need that much precision. Instead, just limit to 1000
files or 5000 blocks, which is likely approximately that much data
anyway.

Closes #10707

Signed-off-by: Jakob Borg <jakob@kastelo.net>
2026-07-01 16:01:46 +00:00
Jakob Borg 35576881ff chore(stcrashreceiver): the "internal" directory exists
Closes #10729

Signed-off-by: Jakob Borg <jakob@kastelo.net>
2026-07-01 17:43:26 +02:00
Syncthing Release Automation 466d30bd70 chore(gui, man, authors): update docs, translations, and contributors 2026-06-29 05:17:21 +00:00
greatroarandJakob Borg 2775f424f2 chore(fs): use x/sys/windows where possible (#10766)
Gets rid of an import of "unsafe".

---------

Signed-off-by: greatroar <61184462+greatroar@users.noreply.github.com>
Signed-off-by: Jakob Borg <jakob@kastelo.net>
Co-authored-by: Jakob Borg <jakob@kastelo.net>
2026-06-28 08:00:08 +02:00
Jakob Borg 44cbfcad56 fix(sqlite): update last migration to set schema version, counts (#10768)
The counts needs to be modified manually since we're not running
triggers during migrations. The schema version needed to be bumped.

Signed-off-by: Jakob Borg <jakob@kastelo.net>
2026-06-26 07:22:40 +02:00
Jakob Borg e4d08b336e fix(config): remove extraneous defaults setting while unmarshalling folder options (fixes #10746, fixes #10389) (#10763)
This broke PATCH on folders. Any place that needs the defaults should
set them prior to unmarshal.

Signed-off-by: Jakob Borg <jakob@kastelo.net>
2026-06-23 07:57:36 +02:00
Jakob Borg 97cb72a608 chore(versioner): attempt to prevent blatantly unsafe external versioner commands (fixes #10721) (#10722)
While preparing the command, attempt to verify that the template
expansion happens in a way that will result in a non-shell-injection
command. I don't presume to say that this is a 100% prevention, and the
script itself can always do dumb shit with the file path later.
Nonetheless, we should make a best-effort attempt.

Equally, this could generate false positives for commands that are
strangely written but in fact safe. I think this is acceptable; external
versioning is currently used by approximately 0.02% of users, and
presumably most of them have a setup that is sane.

---------

Signed-off-by: Jakob Borg <jakob@kastelo.net>
2026-06-23 07:48:53 +02:00
Simon Frei c9236b1adc chore: stop treating dirs as having size 128 (#10750)
Resp. directories for the database, and both dirs and symlinks for
`FileInfo.FileSize`. Instead handle it in the UI progress percentage. We
even already have special cases there for deletions, might as well
handle directories just like any other zero-sized needed item there.

This went through the very thorough testing of running it on my laptop,
the migration was applied and it seemed to be working fine after.

---------

Signed-off-by: Simon Frei <freisim93@gmail.com>
2026-06-22 20:32:03 +02:00
Jakob Borg 5313c75eba chore(model): increase default value for num hashers (#10761) 2026-06-22 12:53:03 +02:00
Syncthing Release Automation 40febaa811 chore(gui, man, authors): update docs, translations, and contributors 2026-06-22 05:29:45 +00:00
Jakob Borg 92ff9ac078 chore(osutil): fixup test for symlinked folder root (#10758)
This test was incorrectly rewritten to use fakefs, which doesn't really
test what it's supposed to test.

Signed-off-by: Jakob Borg <jakob@kastelo.net>
2026-06-21 19:48:15 +02:00
Henrik Bråthen f7f2e7c70b fix(folder): check if context canceled when scanning (fixes #10363) (#10757)
During initial folder scan and the scan is interrupted with a
context.Canceled error, it was previously logged as a "Failed initial
scan" error, which can be misleading

The change adds a explicit check for context.Canceled and silently
ignores it (fixes #10363)

Signed-off-by: Henrik Bråthen <henrikbs@proton.me>
2026-06-21 18:23:37 +02:00
Syncthing Release Automation 3d168d6e44 chore(gui, man, authors): update docs, translations, and contributors 2026-06-15 05:28:39 +00:00
Jakob Borg 8cc4c4ffb3 build: use Go 1.26 for Windows as well (#10744)
Signed-off-by: Jakob Borg <jakob@kastelo.net>
2026-06-13 14:01:46 +02:00
Jakob Borg f1d631d66e fix(protocol): loosen restriction on size of directory entries (#10743)
The synthetic directory size must be permitted.

Ref #10737.

---------

Signed-off-by: Jakob Borg <jakob@kastelo.net>
2026-06-13 06:31:26 +00:00
Jakob Borg ee275fee65 build(deps): update dependencies (#10740)
Signed-off-by: Jakob Borg <jakob@kastelo.net>
2026-06-12 08:59:50 +02:00
Jakob Borg 6606e6b4d3 fix(protocol): be more stringent about blocks in non-file entries (#10737)
Directories, but also symlinks, should not have blocks or a size.

Signed-off-by: Jakob Borg <jakob@kastelo.net>
2026-06-11 18:52:24 +02:00
Jakob Borg f6428af4c8 fix(protocol): always expect & validate block hash in requests (#10738)
Verify that block requests have a hash and that it's correct. This helps
prevent certain races and ensure that only expected data is ever
returned in response to a request.

(In Syncthing prior to 1.28.1 the block hash was omitted for encrypted
requests from trusted devices. This breaks compatibility with that
specific config on those versions.)

---------

Signed-off-by: Jakob Borg <jakob@kastelo.net>
2026-06-11 18:51:25 +02:00
Jakob Borg a5cbeeafea fix(fs, model): improve symlink resilience in file shortcut (#10739)
Ensure file was a file before the shortcut as well as after... (This was
implied when talking to a correct implementation, but not enforced.)

Make our file opening operations safe by default by ensuring the last
path component is not a symlink.

---------

Signed-off-by: Jakob Borg <jakob@kastelo.net>
2026-06-11 18:50:11 +02:00
Syncthing Release Automation 5dbf809a4c chore(gui, man, authors): update docs, translations, and contributors 2026-06-08 05:17:05 +00:00
e16b8013a6 chore(db, model): separate methods to drop a device vs its files (#10480)
Fixes #10469

---------

Signed-off-by: Simon Frei <freisim93@gmail.com>
Signed-off-by: Jakob Borg <jakob@kastelo.net>
Co-authored-by: Marcus B Spencer <marcus@marcusspencer.us>
Co-authored-by: Jakob Borg <jakob@kastelo.net>
2026-06-04 18:28:19 +00:00
Jakob Borg 6df85dc95c fix: let umask do the thing (#10723)
We had a few places where we had perhaps too much of an opinion on the
permissions on created files and directories, sometimes fuled by a
misconception about how permissions work in both Unix and Windows. Recap
on the ground rules:

- On all unixes, all file & directory creation (`Mkdir`, `MkdirAll`,
`Create`, `WriteFile`, `Open`) has the given permission bits filtered
via the user's umask. The proper permissions for us to use are in almost
all cases 0o666 for files and 0o777 for directories, strange as that may
look at the call site.
- On Windows, there is no umask but in turn all of the permission bits
except the user write bit are ignored. The absence of user write bit is
converted into the read only attribute. This means that what is proper
for Unix above is also proper for Windows.
- We make an exception when creating files for certificate keys and the
config / database directories, as those contain secrets we think should remain closed
even if the user generally collaborates with other users on the system.

(Also removal of a bugfixed copy of MkdirAll for Windows that hasn't
been necessary for a few years.)

---------

Signed-off-by: Jakob Borg <jakob@kastelo.net>
2026-06-03 10:54:04 +02:00
Jakob Borg f93306c819 fix(gui): properly escape filenames in fancytree
Signed-off-by: Jakob Borg <jakob@kastelo.net>
2026-06-02 16:54:21 +02:00
Jakob Borg 9cdff04877 fix(connections): do not report connection metrics for self (ref #10509) (#10724)
Makes the metrics easier to interpret.

Signed-off-by: Jakob Borg <jakob@kastelo.net>
2026-06-02 14:01:03 +02:00
ShabloneandElias 9bb5f9b4dd fix: on Windows don't allocate console if not opened inside one (#10726)
This change allows Syncthing to be launched from Explorer without
showing a console window, while preserving the existing command-line
behavior.
Previously, launching syncthing.exe from Explorer would always allocate
a console window, which could only be hidden later by using
`--no-console`. It was not possible to avoid console allocation entirely
without introducing other issues.

On Windows 24H2 and later a new application manifest allows us to
achieve it. See [console allocation
policy](https://learn.microsoft.com/en-us/windows/console/console-allocation-policy)
This manifest is built into a syso-file by `goversioninfo`, which is
already used to generate Windows resource files consumed by the Go
compiler.

**Note1:** On Windows 24H2 and later, no console is allocated when
Syncthing is launched from Explorer, even if `--no-console` is set to
`False`. It can still be used as a CLI tool as usual if you call it from
console.

**Note2:** The content of the manifest file may not be formatted. Even a
`newline` would break it.

### Testing

Tested on Windows 11 25H2: No console visible from explorer. CLI works
as usual.

Ref #8046, ref #10633, ref #10481, ref #10600

Signed-off-by: Elias <1elias.bauer@gmail.com>
Co-authored-by: Elias <1elias.bauer@gmail.com>
2026-06-02 14:00:38 +02:00
Syncthing Release Automation 8961f642c6 chore(gui, man, authors): update docs, translations, and contributors 2026-06-01 05:22:49 +00:00
ShabloneandElias d91f8849a2 chore(syncthing): open URLs via Windows API instead via cmd.exe (#10712)
### Purpose
On Windows replace `cmd.exe /C start` with direct `ShellExecute` API for
opening the webpage.

The previous implementation used `exec.Command("cmd.exe", "/C", "start
"+url)` which spawns two extra processes (cmd.exe → start). Launching
cmd.exe resulted in a shortly visible terminal.

Both
-`start`
-and another alternative `exec.Command("rundll32",
"url.dll,FileProtocolHandler", url).Start()`
are just wrappers for `ShellExecute`. So this implementation is even
more direct

### Testing

I executed the compiled syncthing.exe on Windows 11, both from explorer
and console. The webpage opened as expected.

### Screenshots

N/A.

### Documentation

N/A

## Authorship

Name: Elias @Shablone
Email: [1elias.bauer@gmail.com](mailto:1elias.bauer@gmail.com)

Signed-off-by: Elias <1elias.bauer@gmail.com>
Co-authored-by: Elias <1elias.bauer@gmail.com>
2026-05-25 21:37:10 +00:00
Jakob Borg 6be1ff8480 fix(protocol): handle zero-size requests (fixes #10709) (#10710)
- Allow zero-sized requests since they are sent by all current versions
of Syncthing.
- Stop sending zero-sized requests since that's stupid.

---------

Signed-off-by: Jakob Borg <jakob@kastelo.net>
2026-05-25 15:22:07 +02:00
Syncthing Release Automation 3ec73403c1 chore(gui, man, authors): update docs, translations, and contributors 2026-05-25 05:12:14 +00:00
mattn 8ca3cca0a0 chore: use path/filepath for local file system paths (#10705)
Build Syncthing / Gather common facts (push) Successful in 32s
Build Syncthing / Build and test (~1.25.0, macos-latest) (push) Has been cancelled
Build Syncthing / Build and test (~1.25.0, windows-latest) (push) Has been cancelled
Build Syncthing / Build and test (~1.26.0, macos-latest) (push) Has been cancelled
Build Syncthing / Build and test (~1.26.0, ubuntu-latest) (push) Has been cancelled
Build Syncthing / Build and test (~1.26.0, windows-latest) (push) Has been cancelled
Build Syncthing / Basic checks passed (push) Has been cancelled
Build Syncthing / Package for Windows (push) Has been cancelled
Build Syncthing / Build and test (~1.25.0, ubuntu-latest) (push) Has been cancelled
Build Syncthing / Codesign for Windows (push) Has been cancelled
Build Syncthing / Package for Linux (common) (push) Has been cancelled
Mirrors / Mirror to Codeberg (push) Has been cancelled
Build Syncthing / Package for Linux (other) (push) Has been cancelled
Build Syncthing / Package for Linux (mips) (push) Has been cancelled
Build Syncthing / Package for illumos (push) Has been cancelled
Build Syncthing / Package for macOS (push) Has been cancelled
Build Syncthing / Notarize for macOS (push) Has been cancelled
Build Syncthing / Package cross compiled (push) Has been cancelled
Build Syncthing / Package source code (push) Has been cancelled
Build Syncthing / Sign for upgrade (push) Has been cancelled
Build Syncthing / Package for Debian (push) Has been cancelled
Build Syncthing / Publish nightly build (push) Has been cancelled
Build Syncthing / Publish release files (push) Has been cancelled
Build Syncthing / Publish APT (push) Has been cancelled
Build Syncthing / Build and push Docker images (GHCR) (Dockerfile, syncthing, syncthing) (push) Has been cancelled
Build Syncthing / Build and push Docker images (GHCR) (Dockerfile.stdiscosrv, discosrv, stdiscosrv) (push) Has been cancelled
Build Syncthing / Build and push Docker images (GHCR) (Dockerfile.strelaysrv, relaysrv, strelaysrv) (push) Has been cancelled
Build Syncthing / Sync images to Docker hub (push) Has been cancelled
Build Syncthing / Run govulncheck (push) Has been cancelled
Build Syncthing / Run golangci-lint (push) Has been cancelled
Build Syncthing / Run meta checks (push) Has been cancelled
### Purpose

`path` is for slash-separated paths (URLs, BEP protocol); local file
system paths should use `path/filepath`. Fixed in
`cmd/stdiscosrv/database.go` (3 sites) and
`internal/db/sqlite/db_test.go` (1 site).

### Testing

`go build ./cmd/stdiscosrv/...` and `go vet` pass.

Signed-off-by: Yasuhiro Matsumoto <mattn.jp@gmail.com>
2026-05-23 22:31:33 +02:00
mattn 0c489f4ae2 fix(stcrashreceiver): close source loader responses on errors (#10704)
Fix a response body leak in `githubSourceCodeLoader.Load` where the body
was not closed when the HTTP status was non-200.

Signed-off-by: Yasuhiro Matsumoto <mattn.jp@gmail.com>
2026-05-23 13:50:03 +00:00
Jakob Borg deb1e5b38a Revert "build: temporarily disable illumos for release"
This reverts commit c0c401efeb.
2026-05-23 12:00:07 +02:00
Jakob Borg 49a2688caa Merge branch 'infrastructure'
* infrastructure:
  build: let infra containers builds fail individually
  chore(ur): move structs to reduce dependency chain
  chore(stcrashreceiver): add profiler on metrics port
  chore(stcrashreceiver): compact diskstore in-memory representation
  chore(stcrashreceiver): better source cache & metrics
  chore(stcrashreceiver): metrics on ignore matches
2026-05-23 09:36:25 +02:00
Jakob Borg 05b4f6abda build: let infra containers builds fail individually
Signed-off-by: Jakob Borg <jakob@kastelo.net>
2026-05-23 09:18:15 +02:00
Jakob Borg 9152d7fb2f chore(ur): move structs to reduce dependency chain
lib/ur brings in a lot of dependencies we don't need in e.g.
stcrashreceiver, who only needs the small failure reporting structs.
Make those part of the lean `contract` package instead.

Signed-off-by: Jakob Borg <jakob@kastelo.net>
2026-05-23 09:13:47 +02:00
Jakob Borg 4404b4dfb4 chore(stcrashreceiver): add profiler on metrics port
Signed-off-by: Jakob Borg <jakob@kastelo.net>
2026-05-23 08:51:34 +02:00
Jakob Borg b537090d91 chore(stcrashreceiver): compact diskstore in-memory representation
Signed-off-by: Jakob Borg <jakob@kastelo.net>
2026-05-23 08:51:34 +02:00
Jakob Borg 79423edbdf chore(stcrashreceiver): better source cache & metrics
Signed-off-by: Jakob Borg <jakob@kastelo.net>
2026-05-23 08:51:34 +02:00
Jakob Borg 33075974cb chore(stcrashreceiver): metrics on ignore matches
Signed-off-by: Jakob Borg <jakob@kastelo.net>
2026-05-23 08:51:32 +02:00
Jakob Borg 8a3a06f7ca build(deps): x/net for govulncheck (#10703)
Signed-off-by: Jakob Borg <jakob@kastelo.net>
2026-05-23 06:48:21 +00:00
Jakob Borg d0b35021c6 chore(syncthing): include runtime context in GC crashes (#10702)
The runtime prints a lot of context for crashes due to bad pointers etc,
which is required to understand the crash, but this context comes before
the `fatal error: ...` line. Currently those lines get filtered out and
not included in the crash report. This change modifies the criteria so
that we start collecting crash data also at a line that begins with
`runtime:`, and tweaks the parsing later to look for the specific
`panic:` or `fatal error:` which may come later as the subject.

---------

Signed-off-by: Jakob Borg <jakob@kastelo.net>
2026-05-23 08:40:43 +02:00
Jakob Borg 6322091462 fix(discover): only announce wildcard for TCP punching when listening on wildcard address (fixes #10503) (#10691)
If we aren't announcing e.g. tcp://0.0.0.0:22000 then also do not
announce tcp://0.0.0.0:0.

Signed-off-by: Jakob Borg <jakob@kastelo.net>
2026-05-23 06:37:48 +00:00
Jakob Borg 5464970c5d fix(versioner): ensure user read/write/execute on archived dirs (fixes #10532) (#10696)
This makes sure the user running Syncthing, and hence Synchting itself,
has read/write/execute on directories in .stversions. The other
permission bits remain copied from the source directory, ensuring
whatever group and other permissions were set remain in effect.

Closes #10695.

---------

Signed-off-by: Jakob Borg <jakob@kastelo.net>
2026-05-23 06:21:04 +00:00
Jakob Borg 3962a23723 fix(syncthing): properly upgrade via REST when Syncthing is running (fixes #10697) (#10699)
The locking logic for upgrades got inverted in the lockfile changes. If
we got the lock it means Syncthing wasn't already running, so we can do
a direct upgrade. If we failed to get the lock it means Syncthing was
running and we should tell the REST interface to do the upgrade.

Signed-off-by: Jakob Borg <jakob@kastelo.net>
2026-05-21 10:15:18 +02:00
Jakob Borg feaa90408e Merge branch 'infrastructure'
* infrastructure:
  fix(stcrashreceiver): allow extra pre/post data in version line
  chore(stcrashreceiver): improve logging
  chore(stdiscosrv): prewarm counters at startup
2026-05-21 09:57:46 +02:00
Jakob Borg a8ed6e4855 fix(stcrashreceiver): allow extra pre/post data in version line
Signed-off-by: Jakob Borg <jakob@kastelo.net>
2026-05-19 08:46:22 +02:00
Jakob Borg 5b1e1c0520 chore(stcrashreceiver): improve logging
Signed-off-by: Jakob Borg <jakob@kastelo.net>
2026-05-19 08:46:21 +02:00
Jakob Borg c17be06192 chore(stdiscosrv): prewarm counters at startup
Signed-off-by: Jakob Borg <jakob@kastelo.net>
2026-05-18 23:40:38 +02:00
124 changed files with 1754 additions and 1438 deletions
@@ -22,6 +22,7 @@ jobs:
if: github.repository_owner == 'syncthing'
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
pkg:
- stcrashreceiver
+3 -3
View File
@@ -164,7 +164,7 @@ jobs:
needs:
- build-test
- package-linux
# - package-illumos
- package-illumos
- package-cross
- package-source
- package-debian
@@ -192,7 +192,7 @@ jobs:
- uses: actions/setup-go@v6
with:
go-version: "~1.25.7" # temporarily stay on Go 1.25 due to linker error on Go 1.26
go-version: ${{ needs.facts.outputs.go-version }}
cache: false
- uses: mlugg/setup-zig@d1434d08867e3ee9daa34448df10607b98908d29 # v2
@@ -646,7 +646,7 @@ jobs:
needs:
- codesign-windows
- package-linux
# - package-illumos
- package-illumos
- package-macos
- package-cross
- package-source
+8
View File
@@ -50,6 +50,7 @@ Alexandre Alves <alexandrealvesdb.contact@gmail.com>
Aman Gupta <aman@tmm1.net>
Andreas Sommer <andreas.sommer87@googlemail.com>
andresvia <andres.via@gmail.com>
Andrew Gunnerson <accounts+github@chiller3.com>
Andrew Rabert (nvllsvm) <ar@nullsum.net> <6550543+nvllsvm@users.noreply.github.com>
Andrey D (scienmind) <scintertech@cryptolab.net> <scienmind@users.noreply.github.com>
andyleap <andyleap@gmail.com>
@@ -116,6 +117,7 @@ Dmitry Saveliev (dsaveliev) <d.e.saveliev@gmail.com>
domain <32405309+szu17dmy@users.noreply.github.com>
Domenic Horner <domenic@tgxn.net>
Dominik Heidler (asdil12) <dominik@heidler.eu>
Elias <1elias.bauer@gmail.com>
Elias Jarlebring (jarlebring) <jarlebring@gmail.com>
Elliot Huffman <thelich2@gmail.com>
Emil Hessman (ceh) <emil@hessman.se>
@@ -133,6 +135,7 @@ Felix Lampe <mail@flampe.de>
Felix Unterpaintner (bigbear2nd) <bigbear2nd@gmail.com>
Francois-Xavier Gsell (zukoo) <fxgsell@gmail.com>
Frank Isemann (fti7) <frank@isemann.name>
Frederic <1665799+Finomosec@users.noreply.github.com>
Gahl Saraf <saraf.gahl@gmail.com> <gahl@raftt.io>
georgespatton <georgespatton@users.noreply.github.com>
ghjklw <malo@jaffre.info>
@@ -148,6 +151,7 @@ HansK-p <42314815+HansK-p@users.noreply.github.com>
Harrison Jones (harrisonhjones) <harrisonhjones@users.noreply.github.com>
Hazem Krimi <me@hazemkrimi.tech>
Heiko Zuerker (Smiley73) <heiko@zuerker.org>
Henrik Bråthen <henrikbs@proton.me>
Hireworks <129852174+hireworksltd@users.noreply.github.com>
Hugo Locurcio <hugo.locurcio@hugo.pro>
Iain Barnett <iainspeed@gmail.com>
@@ -216,6 +220,7 @@ Matic Potočnik <hairyfotr@gmail.com>
Matt Burke (burkemw3) <mburke@amplify.com> <burkemw3@gmail.com>
Matt Robenolt <matt@ydekproductions.com>
Matteo Ruina <matteo.ruina@gmail.com>
mattn <mattn.jp@gmail.com>
Maurizio Tomasi <ziotom78@gmail.com>
Max <github@germancoding.com>
Max Schulze (kralo) <max.schulze@online.de> <kralo@users.noreply.github.com>
@@ -274,6 +279,7 @@ red_led <red-led@users.noreply.github.com>
Robert Carosi (nov1n) <robert@carosi.nl>
Roberto Santalla <roobre@users.noreply.github.com>
Robin Schoonover <robin@cornhooves.org>
Rohit Tanwar <31792358+rohitanwar@users.noreply.github.com>
Roman Zaynetdinov (zaynetro) <romanznet@gmail.com>
rubenbe <github-com-00ff86@vandamme.email>
Ruslan Yevdokymov <38809160+ruslanye@users.noreply.github.com>
@@ -286,6 +292,7 @@ Sergey Mishin (ralder) <ralder@yandex.ru>
Sertonix <83883937+Sertonix@users.noreply.github.com>
Severin von Wnuck-Lipinski <ss7@live.de>
Shaarad Dalvi <60266155+shaaraddalvi@users.noreply.github.com> <shdalv@microsoft.com>
Shablone <20610621+Shablone@users.noreply.github.com>
Shivam Kumar <155747305+maishivamhoo123@users.noreply.github.com>
Simon Mwepu <simonmwepu@gmail.com>
Simon Pickup <simon@pickupinfinity.com>
@@ -298,6 +305,7 @@ Sven Bachmann <dev@mcbachmann.de>
Sébastien WENSKE <sebastien@wenske.fr>
Tao <mail@steadytao.com>
Taylor Khan (nelsonkhan) <nelsonkhan@gmail.com>
tbodt <tbodt@tbodt.com>
Terrance <git@terrance.allofti.me>
TheCreeper <TheCreeper@users.noreply.github.com>
Thomas <9749173+uhthomas@users.noreply.github.com>
+18 -33
View File
@@ -1,25 +1,14 @@
## Reporting Bugs
## Reporting Bugs & Feature Requests
Please file bugs in the [GitHub Issue
Tracker](https://github.com/syncthing/syncthing/issues). Include at
least the following:
For general discussion and troubleshooting, please use the [Support
Forum](https://forum.syncthing.net/). For actionable bug reports and feature
requests, please file issues in the [GitHub Issue
Tracker](https://github.com/syncthing/syncthing/issues), using one of the
provided templates.
- What happened
- What did you expect to happen instead of what *did* happen, if it's
not crazy obvious
- What operating system, operating system version and version of
Syncthing you are running
- The same for other connected devices, where relevant
- Screenshot if the issue concerns something visible in the GUI
- Console log entries, where possible and relevant
If you're not sure whether something is relevant, erring on the side of
too much information will never get you yelled at. :)
:warning: Do not submit AI generated issues or comments. Report any details
you experienced as a human, and let anyone who wants to perform AI analysis
do so on their own.
## Contributing Translations
@@ -42,20 +31,17 @@ repository](https://github.com/syncthing/docs).
## Contributing Code
Every contribution is welcome. If you want to contribute but are unsure
where to start, any open issues are fair game! Here's a short rundown of
what you need to keep in mind:
We welcome contributions. However:
- Don't worry. You are not expected to get everything right on the first
attempt, we'll guide you through it.
:warning: We do not accept contributions that are wholly or mostly AI
generated. You may assume that we are fully capable of prompting an AI
ourselves when so inclined.
- Make sure there is an
[issue](https://github.com/syncthing/syncthing/issues) that describes the
change you want to do. If the thing you want to do does not have an issue
yet, please file one before starting work on it.
- Fork the repository and make your changes in a new branch. Once it's ready
for review, create a pull request.
:warning: We do not accept unsolicited pull requests from new contributors.
Most such contributions fall afoul of the previous rule. If yours does not,
please post a message to the relevant issue with a link to your branch and a
short description of the reasoning behind it. We'll take care of onboarding
you.
### Authorship
@@ -205,4 +191,3 @@ otherwise stated this means MPLv2, but there are exceptions:
Regardless of the license in effect, you retain the copyright to your
contribution.
+8
View File
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<application>
<windowsSettings>
<consoleAllocationPolicy xmlns="http://schemas.microsoft.com/SMI/2024/WindowsSettings">detached</consoleAllocationPolicy>
</windowsSettings>
</application>
</assembly>
+15 -6
View File
@@ -718,7 +718,7 @@ func shouldBuildSyso(dir string) (string, error) {
}
jsonPath := filepath.Join(dir, "versioninfo.json")
err = os.WriteFile(jsonPath, bs, 0o644)
err = os.WriteFile(jsonPath, bs, 0o666)
if err != nil {
return "", errors.New("failed to create " + jsonPath + ": " + err.Error())
}
@@ -732,9 +732,18 @@ func shouldBuildSyso(dir string) (string, error) {
sysoPath := filepath.Join(dir, "cmd", "syncthing", "resource.syso")
// See https://github.com/josephspurrier/goversioninfo#command-line-flags
arm := strings.HasPrefix(goarch, "arm")
a64 := strings.Contains(goarch, "64")
if _, err := runError("goversioninfo", "-o", sysoPath, fmt.Sprintf("-arm=%v", arm), fmt.Sprintf("-64=%v", a64)); err != nil {
// For manifest see https://learn.microsoft.com/en-us/windows/console/console-allocation-policy
isARM := strings.HasPrefix(goarch, "arm")
is64Bit := strings.Contains(goarch, "64")
args := []string{
"-manifest=assets/windows/syncthing.exe.manifest", // console-allocation-policy
"-o", sysoPath, // output path
fmt.Sprintf("-arm=%v", isARM),
fmt.Sprintf("-64=%v", is64Bit),
}
if _, err := runError("goversioninfo", args...); err != nil {
return "", errors.New("failed to create " + sysoPath + ": " + err.Error())
}
@@ -774,7 +783,7 @@ func copyFile(src, dst string, perm os.FileMode) error {
}
copy:
os.MkdirAll(filepath.Dir(dst), 0o777)
os.MkdirAll(filepath.Dir(dst), os.ModePerm)
if err := os.WriteFile(dst, in, perm); err != nil {
return err
}
@@ -1423,7 +1432,7 @@ func writeCompatJSON() {
continue
}
bs, _ := json.MarshalIndent(e, "", " ")
if err := os.WriteFile("compat.json", bs, 0o644); err != nil {
if err := os.WriteFile("compat.json", bs, 0o666); err != nil {
log.Fatal("Writing compat.json:", err)
}
return
@@ -0,0 +1,76 @@
2026-05-21 10:03:01 INF syncthing v2.1.1-dev.9.gb3b7d228.dirty-morecrashrep "Hafnium Hornet" (go1.26.3 darwin-arm64) jb@jbo-m3wl72rv 2026-05-21 07:58:11 UTC [stnoupgrade] (log.pkg=main)
2026-05-21 10:03:01 INF No automatic upgrades; STNOUPGRADE environment variable defined (log.pkg=main)
2026-05-21 10:03:01 INF Calculated our device ID (device=I6KAH76-66SLLLB-5PFXSOA-UFJCDZC-YAOMLEK-CP2GB32-BV5RQST-3PSROAU log.pkg=syncthing)
2026-05-21 10:03:01 INF Overall rate limit in use (send="is unlimited" recv="is unlimited" log.pkg=connections)
2026-05-21 10:03:01 INF Using discovery mechanism (identity="IPv4 local broadcast discovery on port 21027" log.pkg=discover)
2026-05-21 10:03:01 INF Using discovery mechanism (identity="IPv6 local multicast discovery on address [ff12::8384]:21027" log.pkg=discover)
2026-05-21 10:03:01 INF TCP listener starting (address=127.0.0.1:22001 log.pkg=connections)
2026-05-21 10:03:01 INF Ready to synchronize (folder.id=default folder.type=sendreceive log.pkg=model)
2026-05-21 10:03:01 INF QUIC listener starting (address=127.0.0.1:22001 log.pkg=connections)
2026-05-21 10:03:01 INF GUI and API listening (address=127.0.0.1:8081 log.pkg=api)
...
2026-05-21 10:03:01 INF Access the GUI via the following URL: http://127.0.0.1:8081/ (log.pkg=api)
2026-05-21 10:03:01 INF Loaded configuration (name=s1 log.pkg=syncthing)
2026-05-21 10:03:01 INF Loaded peer device configuration (device=MRIW7OK name=s2 address="[tcp://127.0.0.1:22002 quic://127.0.0.1:22002]" log.pkg=syncthing)
2026-05-21 10:03:01 INF Completed initial scan (folder.id=default folder.type=sendreceive log.pkg=model)
0xee9de1fe260
2026-05-21 10:03:02 INF Measured hashing performance (perf="2789.71 MB/s" log.pkg=syncthing)
Panic at 2026-05-21T10:03:02+02:00
runtime: marked free object in span 0x108b34d20, elemsize=8 freeindex=34 (bad use of unsafe.Pointer or having race conditions? try -d=checkptr or -race)
0xee9de1fe000 alloc marked
0xee9de1fe008 alloc marked
...
0xee9de1fe250 free unmarked
0xee9de1fe258 free unmarked
0xee9de1fe260 free marked zombie
7 6 5 4 3 2 1 0 f e d c b a 9 8 0123456789abcdef
00000ee9de1fe260: 00000000 00000000 ........
0xee9de1fe268 free unmarked
0xee9de1fe270 free unmarked
...
0xee9de1fff60 free unmarked
0xee9de1fff68 free unmarked
0xee9de1fff70 free unmarked
0xee9de1fff78 free unmarked
fatal error: found pointer to free object
runtime stack:
runtime.throw({0x105881781?, 0x8?})
runtime/panic.go:1229 +0x38 fp=0x16bf82bb0 sp=0x16bf82b80 pc=0x104f0ca48
runtime.(*mspan).reportZombies(0x108b34d20)
runtime/mgcsweep.go:893 +0x314 fp=0x16bf82c30 sp=0x16bf82bb0 pc=0x104ec10b4
runtime.(*sweepLocked).sweep(0x16bf82d88?, 0x0)
runtime/mgcsweep.go:673 +0xbd0 fp=0x16bf82d50 sp=0x16bf82c30 pc=0x104ec0840
runtime.(*mcentral).uncacheSpan(0x16bf82db8?, 0x104ea4954?)
runtime/mcentral.go:237 +0xbc fp=0x16bf82d80 sp=0x16bf82d50 pc=0x104eaac3c
runtime.(*mcache).releaseAll(0x1089a85f0)
runtime/mcache.go:322 +0x188 fp=0x16bf82df0 sp=0x16bf82d80 pc=0x104eaa4e8
runtime.(*mcache).prepareForSweep(0x1089a85f0)
runtime/mcache.go:366 +0x4c fp=0x16bf82e20 sp=0x16bf82df0 pc=0x104eaa61c
runtime.gcMarkTermination.func4(0xee9de005808)
runtime/mgc.go:1546 +0x24 fp=0x16bf82e50 sp=0x16bf82e20 pc=0x104f076e4
runtime.forEachPInternal(0x10656f798)
runtime/proc.go:2167 +0x178 fp=0x16bf82ee0 sp=0x16bf82e50 pc=0x104eda728
runtime.gcMarkTermination.forEachP.func7()
runtime/proc.go:2126 +0x40 fp=0x16bf82f10 sp=0x16bf82ee0 pc=0x104eb3130
runtime.systemstack(0x7fc000)
runtime/asm_arm64.s:399 +0x68 fp=0x16bf82f20 sp=0x16bf82f10 pc=0x104f12888
goroutine 84 gp=0xee9de45c1e0 m=3 mp=0xee9de019008 [flushing proc caches]:
runtime.systemstack_switch()
runtime/asm_arm64.s:347 +0x8 fp=0xee9de805c40 sp=0xee9de805c30 pc=0x104f12808
runtime.forEachP(...)
runtime/proc.go:2112
runtime.gcMarkTermination({0xc0?, 0x1331f928480ca?, 0xc?, 0x0?})
runtime/mgc.go:1545 +0x5f4 fp=0xee9de805e80 sp=0xee9de805c40 pc=0x104eb28c4
runtime.gcMarkDone()
runtime/mgc.go:1173 +0x364 fp=0xee9de805f20 sp=0xee9de805e80 pc=0x104eb1bc4
runtime.gcBgMarkWorker(0xee9de341810)
runtime/mgc.go:1912 +0x29c fp=0xee9de805fb0 sp=0xee9de805f20 pc=0x104eb372c
runtime.gcBgMarkStartWorkers.gowrap1()
runtime/mgc.go:1695 +0x20 fp=0xee9de805fd0 sp=0xee9de805fb0 pc=0x104eb3470
runtime.goexit({})
runtime/asm_arm64.s:1447 +0x4 fp=0xee9de805fd0 sp=0xee9de805fd0 pc=0x104f14a04
created by runtime.gcBgMarkStartWorkers in goroutine 1
runtime/mgc.go:1695 +0x134
+17 -7
View File
@@ -42,7 +42,7 @@ type currentFile struct {
}
func (d *diskStore) Serve(ctx context.Context) {
if err := os.MkdirAll(d.dir, 0o700); err != nil {
if err := os.MkdirAll(d.dir, os.ModePerm); err != nil {
log.Println("Creating directory:", err)
return
}
@@ -62,7 +62,7 @@ func (d *diskStore) Serve(ctx context.Context) {
case entry := <-d.inbox:
path := d.fullPath(entry.path)
if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
if err := os.MkdirAll(filepath.Dir(path), os.ModePerm); err != nil {
log.Println("Creating directory:", err)
continue
}
@@ -77,7 +77,7 @@ func (d *diskStore) Serve(ctx context.Context) {
log.Println("Failed to compress crash report:", err)
continue
}
if err := os.WriteFile(path, buf.Bytes(), 0o600); err != nil {
if err := os.WriteFile(path, buf.Bytes(), 0o666); err != nil {
log.Printf("Failed to write %s: %v", entry.path, err)
_ = os.Remove(path)
continue
@@ -136,15 +136,25 @@ func (d *diskStore) Exists(path string) bool {
}
func (d *diskStore) clean() {
for len(d.currentFiles) > 0 && (len(d.currentFiles) > d.maxFiles || d.currentSize > d.maxBytes) {
f := d.currentFiles[0]
numDeleted := 0
for idx := range d.currentFiles {
if len(d.currentFiles)-numDeleted < d.maxFiles && d.currentSize < d.maxBytes {
break
}
f := d.currentFiles[idx]
log.Println("Removing", f.path)
if err := os.Remove(f.path); err != nil {
log.Println("Failed to remove file:", err)
}
d.currentFiles = d.currentFiles[1:]
d.currentSize -= f.size
numDeleted = idx + 1
}
// Compact currentFiles
copy(d.currentFiles, d.currentFiles[numDeleted:])
d.currentFiles = d.currentFiles[:len(d.currentFiles)-numDeleted]
var oldest time.Duration
if len(d.currentFiles) > 0 {
oldest = time.Since(time.Unix(d.currentFiles[0].mtime, 0)).Truncate(time.Minute)
@@ -158,7 +168,7 @@ func (d *diskStore) clean() {
}
func (d *diskStore) inventory() error {
d.currentFiles = nil
d.currentFiles = d.currentFiles[:0]
d.currentSize = 0
err := filepath.Walk(d.dir, func(path string, info os.FileInfo, err error) error {
if err != nil {
+11 -8
View File
@@ -20,6 +20,7 @@ import (
"io"
"log"
"net/http"
"net/http/pprof"
"os"
"path/filepath"
"regexp"
@@ -29,7 +30,7 @@ import (
raven "github.com/getsentry/raven-go"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/syncthing/syncthing/lib/build"
"github.com/syncthing/syncthing/lib/ur"
"github.com/syncthing/syncthing/lib/ur/contract"
)
const maxRequestSize = 1 << 20 // 1 MiB
@@ -89,6 +90,7 @@ func main() {
if params.MetricsListen != "" {
mmux := http.NewServeMux()
mmux.Handle("/metrics", promhttp.Handler())
mmux.HandleFunc("/debug/pprof/", pprof.Index)
go func() {
if err := http.ListenAndServe(params.MetricsListen, mmux); err != nil {
log.Fatalln("HTTP serve metrics:", err)
@@ -123,12 +125,13 @@ func handleFailureFn(dsn, failureDir string, ignore *ignorePatterns) func(w http
return
}
if ignore.match(bs) {
if pat, ok := ignore.match(bs); ok {
metricIgnoreMatchesTotal.WithLabelValues(pat).Inc()
result = "ignored"
return
}
var reports []ur.FailureReport
var reports []contract.FailureReport
err = json.Unmarshal(bs, &reports)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
@@ -176,7 +179,7 @@ func handleFailureFn(dsn, failureDir string, ignore *ignorePatterns) func(w http
}
}
func saveFailureWithGoroutines(data ur.FailureData, failureDir string) (string, error) {
func saveFailureWithGoroutines(data contract.FailureData, failureDir string) (string, error) {
bs := make([]byte, len(data.Description)+len(data.Goroutines))
copy(bs, data.Description)
copy(bs[len(data.Description):], data.Goroutines)
@@ -216,14 +219,14 @@ func loadIgnorePatterns(path string) (*ignorePatterns, error) {
return &ignorePatterns{patterns: patterns}, nil
}
func (i *ignorePatterns) match(report []byte) bool {
func (i *ignorePatterns) match(report []byte) (string, bool) {
if i == nil {
return false
return "", false
}
for _, re := range i.patterns {
if re.Match(report) {
return true
return re.String(), true
}
}
return false
return "", false
}
+20
View File
@@ -37,4 +37,24 @@ var (
Subsystem: "crashreceiver",
Name: "diskstore_oldest_age_seconds",
})
metricSentryReportsTotal = promauto.NewCounterVec(prometheus.CounterOpts{
Namespace: "syncthing",
Subsystem: "crashreceiver",
Name: "sentry_reports_total",
}, []string{"result"})
metricIgnoreMatchesTotal = promauto.NewCounterVec(prometheus.CounterOpts{
Namespace: "syncthing",
Subsystem: "crashreceiver",
Name: "ignore_matches_total",
}, []string{"pattern"})
metricSourceCodeLoadsTotal = promauto.NewCounterVec(prometheus.CounterOpts{
Namespace: "syncthing",
Subsystem: "crashreceiver",
Name: "source_code_loads_total",
}, []string{"result"})
metricSourceCodeCacheSize = promauto.NewGauge(prometheus.GaugeOpts{
Namespace: "syncthing",
Subsystem: "crashreceiver",
Name: "source_code_cache_size",
})
)
+8 -8
View File
@@ -10,6 +10,7 @@ import (
"bytes"
"context"
"errors"
"fmt"
"io"
"log"
"regexp"
@@ -52,11 +53,15 @@ func (s *sentryService) Serve(ctx context.Context) {
pkt, err := parseCrashReport(req.reportID, req.data)
if err != nil {
log.Println("Failed to parse crash report:", err)
metricSentryReportsTotal.WithLabelValues("parse_failure").Inc()
continue
}
if err := sendReport(s.dsn, pkt, req.userID); err != nil {
log.Println("Failed to send crash report:", err)
metricSentryReportsTotal.WithLabelValues("send_failure").Inc()
continue
}
metricSentryReportsTotal.WithLabelValues("success").Inc()
case <-ctx.Done():
return
@@ -69,6 +74,7 @@ func (s *sentryService) Send(reportID, userID string, data []byte) bool {
case s.inbox <- sentryRequest{reportID, userID, data}:
return true
default:
metricCrashReportsTotal.WithLabelValues("overflow").Inc()
return false
}
}
@@ -108,11 +114,10 @@ func parseCrashReport(path string, report []byte) (*raven.Packet, error) {
version, err := build.ParseVersion(string(parts[0]))
if err != nil {
return nil, err
return nil, fmt.Errorf("%w in %q", err, parts[0])
}
report = parts[1]
foundPanic := false
var subjectLine []byte
for {
parts = bytes.SplitN(report, []byte("\n"), 2)
@@ -123,14 +128,9 @@ func parseCrashReport(path string, report []byte) (*raven.Packet, error) {
line := parts[0]
report = parts[1]
if foundPanic {
// The previous line was our "Panic at ..." header. We are now
// at the beginning of the real panic trace and this is our
// subject line.
if bytes.HasPrefix(line, []byte("panic:")) || bytes.HasPrefix(line, []byte("fatal error:")) {
subjectLine = line
break
} else if bytes.HasPrefix(line, []byte("Panic at")) {
foundPanic = true
}
}
+18 -11
View File
@@ -9,26 +9,33 @@ package main
import (
"fmt"
"os"
"path/filepath"
"testing"
)
func TestParseReport(t *testing.T) {
bs, err := os.ReadFile("_testdata/panic.log")
files, err := filepath.Glob("_testdata/*.log")
if err != nil {
t.Fatal(err)
}
for _, file := range files {
bs, err := os.ReadFile(file)
if err != nil {
t.Fatal(err)
}
pkt, err := parseCrashReport("1/2/345", bs)
if err != nil {
t.Fatal(err)
pkt, err := parseCrashReport("1/2/345", bs)
if err != nil {
t.Fatal(err)
}
bs, err = pkt.JSON()
if err != nil {
t.Fatal(err)
}
fmt.Printf("%s\n", bs)
}
bs, err = pkt.JSON()
if err != nil {
t.Fatal(err)
}
fmt.Printf("%s\n", bs)
}
func TestCrashReportFingerprint(t *testing.T) {
+27 -12
View File
@@ -15,23 +15,33 @@ import (
"strings"
"sync"
"time"
lru "github.com/hashicorp/golang-lru/v2"
)
const (
urlPrefix = "https://raw.githubusercontent.com/syncthing/syncthing/"
httpTimeout = 10 * time.Second
urlPrefix = "https://raw.githubusercontent.com/syncthing/syncthing/"
httpTimeout = 10 * time.Second
maxCacheEntries = 1000
)
type cacheKey struct {
version string
file string
}
type githubSourceCodeLoader struct {
mut sync.Mutex
version string
cache map[string]map[string][][]byte // version -> file -> lines
client *http.Client
cache *lru.TwoQueueCache[cacheKey, [][]byte] // version & file -> lines
client *http.Client
}
func newGithubSourceCodeLoader() *githubSourceCodeLoader {
cache, _ := lru.New2Q[cacheKey, [][]byte](maxCacheEntries)
return &githubSourceCodeLoader{
cache: make(map[string]map[string][][]byte),
cache: cache,
client: &http.Client{Timeout: httpTimeout},
}
}
@@ -39,9 +49,6 @@ func newGithubSourceCodeLoader() *githubSourceCodeLoader {
func (l *githubSourceCodeLoader) LockWithVersion(version string) {
l.mut.Lock()
l.version = version
if _, ok := l.cache[version]; !ok {
l.cache[version] = make(map[string][][]byte)
}
}
func (l *githubSourceCodeLoader) Unlock() {
@@ -50,14 +57,16 @@ func (l *githubSourceCodeLoader) Unlock() {
func (l *githubSourceCodeLoader) Load(filename string, line, context int) ([][]byte, int) {
filename = filepath.ToSlash(filename)
lines, ok := l.cache[l.version][filename]
key := cacheKey{version: l.version, file: filename}
lines, ok := l.cache.Get(key)
if !ok {
// Cache whatever we managed to find (or nil if nothing, so we don't try again)
defer func() {
l.cache[l.version][filename] = lines
l.cache.Add(key, lines)
metricSourceCodeCacheSize.Set(float64(l.cache.Len()))
}()
knownPrefixes := []string{"/lib/", "/cmd/"}
knownPrefixes := []string{"/internal/", "/lib/", "/cmd/"}
var idx int
for _, pref := range knownPrefixes {
idx = strings.Index(filename, pref)
@@ -73,19 +82,25 @@ func (l *githubSourceCodeLoader) Load(filename string, line, context int) ([][]b
resp, err := l.client.Get(url)
if err != nil {
fmt.Println("Loading source:", err)
metricSourceCodeLoadsTotal.WithLabelValues("failed").Inc()
return nil, 0
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
fmt.Println("Loading source:", resp.Status)
metricSourceCodeLoadsTotal.WithLabelValues("failed").Inc()
return nil, 0
}
data, err := io.ReadAll(resp.Body)
_ = resp.Body.Close()
if err != nil {
fmt.Println("Loading source:", err.Error())
metricSourceCodeLoadsTotal.WithLabelValues("failed").Inc()
return nil, 0
}
lines = bytes.Split(data, []byte{'\n'})
metricSourceCodeLoadsTotal.WithLabelValues("loaded").Inc()
} else {
metricSourceCodeLoadsTotal.WithLabelValues("cached").Inc()
}
return getLineFromLines(lines, line, context)
+10 -29
View File
@@ -7,21 +7,18 @@
package main
import (
"bytes"
"io"
"log"
"net/http"
"path"
"strings"
"sync"
)
type crashReceiver struct {
store *diskStore
sentry *sentryService
ignore *ignorePatterns
ignoredMut sync.RWMutex
ignored map[string]struct{}
}
func (r *crashReceiver) ServeHTTP(w http.ResponseWriter, req *http.Request) {
@@ -69,12 +66,6 @@ func (r *crashReceiver) serveGet(reportID string, w http.ResponseWriter, _ *http
// serveHead responds to HEAD requests by checking if the named report
// already exists in the system.
func (r *crashReceiver) serveHead(reportID string, w http.ResponseWriter, _ *http.Request) {
r.ignoredMut.RLock()
_, ignored := r.ignored[reportID]
r.ignoredMut.RUnlock()
if ignored {
return // found
}
if !r.store.Exists(reportID) {
http.Error(w, "Not found", http.StatusNotFound)
}
@@ -87,17 +78,7 @@ func (r *crashReceiver) servePut(reportID string, w http.ResponseWriter, req *ht
metricCrashReportsTotal.WithLabelValues(result).Inc()
}()
r.ignoredMut.RLock()
_, ignored := r.ignored[reportID]
r.ignoredMut.RUnlock()
if ignored {
result = "ignored_cached"
io.Copy(io.Discard, req.Body)
return // found
}
// Read at most maxRequestSize of report data.
log.Println("Receiving report", reportID)
lr := io.LimitReader(req.Body, maxRequestSize)
bs, err := io.ReadAll(lr)
if err != nil {
@@ -106,14 +87,12 @@ func (r *crashReceiver) servePut(reportID string, w http.ResponseWriter, req *ht
return
}
if r.ignore.match(bs) {
r.ignoredMut.Lock()
if r.ignored == nil {
r.ignored = make(map[string]struct{})
}
r.ignored[reportID] = struct{}{}
r.ignoredMut.Unlock()
first := string(bytes.TrimSpace(bytes.Split(bs, []byte("\n"))[0]))
if pat, ok := r.ignore.match(bs); ok {
metricIgnoreMatchesTotal.WithLabelValues(pat).Inc()
result = "ignored"
log.Printf("Ignored report %s, matched: %s (%s)", reportID[:8], pat, first)
return
}
@@ -121,13 +100,15 @@ func (r *crashReceiver) servePut(reportID string, w http.ResponseWriter, req *ht
// Store the report
if !r.store.Put(reportID, bs) {
log.Println("Failed to store report (queue full):", reportID)
log.Println("Failed to store report (queue full):", reportID[:8])
result = "queue_failure"
}
// Send the report to Sentry
if !r.sentry.Send(reportID, userIDFor(req), bs) {
log.Println("Failed to send report to sentry (queue full):", reportID)
log.Println("Failed to send report to sentry (queue full):", reportID[:8])
result = "sentry_failure"
}
log.Printf("Received report %s (%s)", reportID[:8], first)
}
+1 -1
View File
@@ -52,5 +52,5 @@ func compressAndWrite(bs []byte, fullPath string) error {
gw.Close()
// Create an output file with the compressed report
return os.WriteFile(fullPath, buf.Bytes(), 0o644)
return os.WriteFile(fullPath, buf.Bytes(), 0o666)
}
+26 -10
View File
@@ -3,6 +3,7 @@
package main
import (
"bytes"
"context"
"crypto/tls"
"crypto/x509"
@@ -29,6 +30,7 @@ import (
"github.com/syncthing/syncthing/lib/assets"
"github.com/syncthing/syncthing/lib/build"
"github.com/syncthing/syncthing/lib/geoip"
"github.com/syncthing/syncthing/lib/osutil"
"github.com/syncthing/syncthing/lib/protocol"
"github.com/syncthing/syncthing/lib/rand"
"github.com/syncthing/syncthing/lib/relay/client"
@@ -318,11 +320,12 @@ func handleEndpointFull(rw http.ResponseWriter, r *http.Request) {
relays := make([]*relay, len(permanentRelays)+len(knownRelays))
n := copy(relays, permanentRelays)
copy(relays[n:], knownRelays)
mut.RUnlock()
_ = json.NewEncoder(rw).Encode(map[string][]*relay{
bs, _ := json.Marshal(map[string][]*relay{
"relays": relays,
})
mut.RUnlock()
_, _ = rw.Write(bs)
}
// handleEndpointShort returns the relay list with only the URL.
@@ -332,7 +335,10 @@ func handleEndpointShort(rw http.ResponseWriter, r *http.Request) {
mut.RLock()
relays := make([]relayShort, 0, len(permanentRelays)+len(knownRelays))
for _, r := range append(permanentRelays, knownRelays...) {
for _, r := range permanentRelays {
relays = append(relays, relayShort{URL: slimURL(r.URL)})
}
for _, r := range knownRelays {
relays = append(relays, relayShort{URL: slimURL(r.URL)})
}
mut.RUnlock()
@@ -544,7 +550,7 @@ found:
mut.Unlock()
if err := saveRelays(knownRelaysFile, knownRelays); err != nil {
if err := saveKnownRelays(knownRelaysFile); err != nil {
log.Println("Failed to write known relays: " + err.Error())
}
@@ -607,12 +613,22 @@ func loadRelays(file string, geoip *geoip.Provider) []*relay {
return relays
}
func saveRelays(file string, relays []*relay) error {
var content string
for _, relay := range relays {
content += relay.uri.String() + "\n"
func saveKnownRelays(file string) error {
var buf bytes.Buffer
mut.RLock()
for _, relay := range knownRelays {
fmt.Fprintln(&buf, relay.uri.String())
}
return os.WriteFile(file, []byte(content), 0o777)
mut.RUnlock()
fd, err := osutil.CreateAtomic(file)
if err != nil {
return err
}
if _, err := fd.Write(buf.Bytes()); err != nil {
return err
}
return fd.Close()
}
func createTestCertificate() tls.Certificate {
+4 -4
View File
@@ -15,7 +15,7 @@ import (
"io"
"log/slog"
"os"
"path"
"path/filepath"
"runtime"
"slices"
"strings"
@@ -77,7 +77,7 @@ func newInMemoryStore(dir string, flushInterval time.Duration, blobs blob.Store)
slog.Error("Failed to find database in blob storage", "error", cerr)
return s
}
fd, cerr := os.Create(path.Join(s.dir, "records.db"))
fd, cerr := os.Create(filepath.Join(s.dir, "records.db"))
if cerr != nil {
slog.Error("Failed to create database file", "error", cerr)
return s
@@ -257,7 +257,7 @@ func (s *inMemoryStore) write() (err error) {
}
}()
dbf := path.Join(s.dir, "records.db")
dbf := filepath.Join(s.dir, "records.db")
fd, err := os.Create(dbf + ".tmp")
if err != nil {
return err
@@ -340,7 +340,7 @@ func (s *inMemoryStore) write() (err error) {
}
func (s *inMemoryStore) read() (int, error) {
fd, err := os.Open(path.Join(s.dir, "records.db"))
fd, err := os.Open(filepath.Join(s.dir, "records.db"))
if err != nil {
return 0, err
}
+27 -8
View File
@@ -7,6 +7,8 @@
package main
import (
"net/http"
"github.com/prometheus/client_golang/prometheus"
)
@@ -113,14 +115,11 @@ var (
)
const (
dbOpGet = "get"
dbOpPut = "put"
dbOpMerge = "merge"
dbOpDelete = "delete"
dbResSuccess = "success"
dbResNotFound = "not_found"
dbResError = "error"
dbResUnmarshalError = "unmarsh_err"
dbOpGet = "get"
dbOpPut = "put"
dbOpMerge = "merge"
dbResSuccess = "success"
dbResNotFound = "not_found"
)
func init() {
@@ -132,4 +131,24 @@ func init() {
databaseOperations, databaseOperationSeconds,
databaseWriteSeconds, databaseLastWritten,
retryAfterLevel)
// Prewarm important counters so they're available with zero values at
// startup
apiRequestsTotal.WithLabelValues(http.MethodGet, "200")
apiRequestsTotal.WithLabelValues(http.MethodGet, "404")
apiRequestsTotal.WithLabelValues(http.MethodPost, "204")
apiRequestsTotal.WithLabelValues(http.MethodPost, "400")
apiRequestsTotal.WithLabelValues(http.MethodPost, "403")
lookupRequestsTotal.WithLabelValues("success")
lookupRequestsTotal.WithLabelValues("not_found_ever")
lookupRequestsTotal.WithLabelValues("not_found_recent")
announceRequestsTotal.WithLabelValues("success")
announceRequestsTotal.WithLabelValues("bad_request")
announceRequestsTotal.WithLabelValues("no_certificate")
replicationSendsTotal.WithLabelValues("success")
replicationRecvsTotal.WithLabelValues("success")
}
+1 -1
View File
@@ -167,7 +167,7 @@ func (c *CLI) process(srcFs fs.Filesystem, dstFs fs.Filesystem, path string) err
var plainFd fs.File
if dstFs != nil {
if err := dstFs.MkdirAll(filepath.Dir(plainFi.Name), 0o700); err != nil {
if err := dstFs.MkdirAll(filepath.Dir(plainFi.Name), fs.ModePerm); err != nil {
return fmt.Errorf("%s: %w", plainFi.Name, err)
}
+1 -1
View File
@@ -7,5 +7,5 @@
package main
type buildSpecificOptions struct {
HideConsole bool `name:"no-console" help:"Hide console window" env:"STHIDECONSOLE"`
HideConsole bool `name:"no-console" help:"Hide console window (Always enabled on Windows 11 24H2 and later)" env:"STHIDECONSOLE"`
}
+8 -4
View File
@@ -673,7 +673,7 @@ func auditWriter(auditFile string) io.Writer {
} else {
auditFlags = os.O_WRONLY | os.O_CREATE | os.O_APPEND
}
fd, err = os.OpenFile(auditFile, auditFlags, 0o600)
fd, err = os.OpenFile(auditFile, auditFlags, 0o666)
if err != nil {
slog.Error("Failed to open audit file", slogutil.Error(err))
os.Exit(svcutil.ExitError.AsInt())
@@ -915,10 +915,14 @@ func (u upgradeCmd) Run() error {
case err != nil && !os.IsNotExist(err):
slog.Error("Failed to lock for upgrade", slogutil.Error(err))
os.Exit(1)
case locked:
err = upgradeViaRest()
default:
case locked || os.IsNotExist(err):
// We got the lock, or the config directory didn't exist, so we
// can do a direct upgrade
err = upgrade.To(release)
default:
// We didn't get the lock, because Syncthing was running, so
// upgrade via REST.
err = upgradeViaRest()
}
}
if err != nil {
+2 -2
View File
@@ -233,7 +233,7 @@ func copyStderr(stderr io.Reader, dst io.Writer) {
dst.Write([]byte(line))
if panicFd == nil && (strings.HasPrefix(line, "panic:") || strings.HasPrefix(line, "fatal error:")) {
if panicFd == nil && (strings.HasPrefix(line, "panic:") || strings.HasPrefix(line, "fatal error:") || strings.HasPrefix(line, "runtime:")) {
panicFd, err = os.Create(locations.GetTimestamped(locations.PanicLog))
if err != nil {
slog.Error("Failed to create panic log", slogutil.Error(err))
@@ -479,7 +479,7 @@ func (f *autoclosedFile) ensureOpenLocked() error {
// We open the file for write only, and create it if it doesn't exist.
flags := os.O_WRONLY | os.O_CREATE | os.O_APPEND
fd, err := os.OpenFile(f.name, flags, 0o644)
fd, err := os.OpenFile(f.name, flags, 0o666)
if err != nil {
return err
}
+21 -2
View File
@@ -9,8 +9,27 @@
package main
import "os/exec"
import "golang.org/x/sys/windows"
func openURL(url string) error {
return exec.Command("cmd.exe", "/C", "start "+url).Run()
urlPtr, err := windows.UTF16PtrFromString(url)
if err != nil {
return err
}
verbPtr, err := windows.UTF16PtrFromString("open")
if err != nil {
return err
}
err = windows.ShellExecute(
0, // hwnd
verbPtr, // operation
urlPtr, // file
nil, // parameters
nil, // directory
windows.SW_SHOWNORMAL,
)
return err
}
+25 -28
View File
@@ -4,15 +4,15 @@ go 1.25.0
require (
github.com/AudriusButkevicius/recli v0.0.7
github.com/alecthomas/kong v1.15.0
github.com/alecthomas/kong v1.16.0
github.com/aws/aws-sdk-go v1.55.8
github.com/calmh/incontainer v1.0.0
github.com/calmh/xdr v1.2.0
github.com/ccding/go-stun v0.1.5
github.com/ccding/go-stun v0.1.6
github.com/coreos/go-semver v0.3.1
github.com/d4l3k/messagediff v1.2.1
github.com/getsentry/raven-go v0.2.0
github.com/go-ldap/ldap/v3 v3.4.13
github.com/go-ldap/ldap/v3 v3.4.14
github.com/gobwas/glob v0.2.3
github.com/gofrs/flock v0.13.0
github.com/hashicorp/golang-lru/v2 v2.0.7
@@ -22,17 +22,17 @@ require (
github.com/julienschmidt/httprouter v1.3.0
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
github.com/maruel/panicparse/v2 v2.5.0
github.com/mattn/go-sqlite3 v1.14.44
github.com/mattn/go-sqlite3 v1.14.48
github.com/maxmind/geoipupdate/v6 v6.1.0
github.com/miscreant/miscreant.go v0.0.0-20200214223636-26d376326b75
github.com/oschwald/geoip2-golang v1.13.0
github.com/pierrec/lz4/v4 v4.1.26
github.com/prometheus/client_golang v1.23.2
github.com/pierrec/lz4/v4 v4.1.27
github.com/prometheus/client_golang v1.24.0
github.com/puzpuzpuz/xsync/v3 v3.5.1
github.com/quic-go/quic-go v0.59.0
github.com/rabbitmq/amqp091-go v1.11.0
github.com/quic-go/quic-go v0.60.0
github.com/rabbitmq/amqp091-go v1.13.0
github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9
github.com/shirou/gopsutil/v4 v4.26.4
github.com/shirou/gopsutil/v4 v4.26.6
github.com/syncthing/notify v0.0.0-20250528144937-c7027d4f7465
github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d
github.com/thejerf/suture/v4 v4.0.6
@@ -40,19 +40,19 @@ require (
github.com/vitrun/qart v0.0.0-20160531060029-bf64b92db6b0
github.com/willabides/kongplete v0.4.0
github.com/wlynxg/anet v0.0.5
golang.org/x/crypto v0.51.0
golang.org/x/exp v0.0.0-20260508232706-74f9aab9d74a
golang.org/x/net v0.54.0
golang.org/x/sys v0.44.0
golang.org/x/text v0.37.0
golang.org/x/crypto v0.54.0
golang.org/x/exp v0.0.0-20260718201538-764159d718ef
golang.org/x/net v0.57.0
golang.org/x/sys v0.47.0
golang.org/x/text v0.40.0
golang.org/x/time v0.15.0
google.golang.org/protobuf v1.36.11
modernc.org/sqlite v1.50.0
modernc.org/sqlite v1.54.0
sigs.k8s.io/yaml v1.6.0
)
require (
github.com/Azure/go-ntlmssp v0.1.0 // indirect
github.com/Azure/go-ntlmssp v0.1.1 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/certifi/gocertifi v0.0.0-20210507211836-431795d63e8d // indirect
@@ -62,7 +62,7 @@ require (
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/ebitengine/purego v0.10.0 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect
github.com/go-asn1-ber/asn1-ber v1.5.8 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/google/pprof v0.0.0-20250423184734-337e5dd93bb4 // indirect
@@ -82,8 +82,8 @@ require (
github.com/posener/complete v1.2.3 // indirect
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.66.1 // indirect
github.com/prometheus/procfs v0.16.1 // indirect
github.com/prometheus/common v0.70.0 // indirect
github.com/prometheus/procfs v0.21.1 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/riywo/loginshell v0.0.0-20200815045211-7d26008be1ab // indirect
github.com/rogpeppe/go-internal v1.12.0 // indirect
@@ -93,13 +93,13 @@ require (
github.com/tklauser/go-sysconf v0.3.16 // indirect
github.com/tklauser/numcpus v0.11.0 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
go.yaml.in/yaml/v2 v2.4.2 // indirect
golang.org/x/mod v0.36.0 // indirect
golang.org/x/sync v0.20.0 // indirect
golang.org/x/telemetry v0.0.0-20260508192327-42602be52be6 // indirect
golang.org/x/tools v0.45.0 // indirect
go.yaml.in/yaml/v2 v2.4.4 // indirect
golang.org/x/mod v0.38.0 // indirect
golang.org/x/sync v0.22.0 // indirect
golang.org/x/telemetry v0.0.0-20260708182218-49f421fb7959 // indirect
golang.org/x/tools v0.48.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
modernc.org/libc v1.72.0 // indirect
modernc.org/libc v1.74.1 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect
)
@@ -110,9 +110,6 @@ replace github.com/gobwas/glob v0.2.3 => github.com/calmh/glob v0.0.0-2022061508
// https://github.com/jackpal/gateway/pull/49
replace github.com/jackpal/gateway v1.1.1 => github.com/marbens-arch/gateway v1.1.2-0.20260308173556-c567cc04e7d4
// https://github.com/mattn/go-sqlite3/pull/1338
replace github.com/mattn/go-sqlite3 v1.14.44 => github.com/calmh/go-sqlite3 v1.14.35-0.20260509063420-822b4765116d
tool (
github.com/calmh/xdr/cmd/genxdr
github.com/maxbrunsfeld/counterfeiter/v6
+62 -60
View File
@@ -2,13 +2,13 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/AudriusButkevicius/recli v0.0.7 h1:9zjbYlTupi+W5SJXm2cR2sV2mJAIg1sIfDcsW7hrkPM=
github.com/AudriusButkevicius/recli v0.0.7/go.mod h1:Nhfib1j/VFnLrXL9cHgA+/n2O6P5THuWelOnbfPNd78=
github.com/Azure/go-ntlmssp v0.1.0 h1:DjFo6YtWzNqNvQdrwEyr/e4nhU3vRiwenz5QX7sFz+A=
github.com/Azure/go-ntlmssp v0.1.0/go.mod h1:NYqdhxd/8aAct/s4qSYZEerdPuH1liG2/X9DiVTbhpk=
github.com/Azure/go-ntlmssp v0.1.1 h1:l+FM/EEMb0U9QZE7mKNEDw5Mu3mFiaa2GKOoTSsNDPw=
github.com/Azure/go-ntlmssp v0.1.1/go.mod h1:NYqdhxd/8aAct/s4qSYZEerdPuH1liG2/X9DiVTbhpk=
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
github.com/alecthomas/kong v1.15.0 h1:BVJstKbpO73zKpmIu+m/aLRrNmWwxXPIGTNin9VmLVI=
github.com/alecthomas/kong v1.15.0/go.mod h1:wrlbXem1CWqUV5Vbmss5ISYhsVPkBb1Yo7YKJghju2I=
github.com/alecthomas/kong v1.16.0 h1:g92/kUxBcdcTPOM79yE63viJgtcp5dNyrB3/O2cjYT4=
github.com/alecthomas/kong v1.16.0/go.mod h1:wrlbXem1CWqUV5Vbmss5ISYhsVPkBb1Yo7YKJghju2I=
github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs=
github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e h1:4dAU9FXIyQktpoUAgOJK3OTFc/xug0PCXYCqU0FgDKI=
@@ -19,14 +19,12 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/calmh/glob v0.0.0-20220615080505-1d823af5017b h1:Fjm4GuJ+TGMgqfGHN42IQArJb77CfD/mAwLbDUoJe6g=
github.com/calmh/glob v0.0.0-20220615080505-1d823af5017b/go.mod h1:91K7jfEsgJSyfSrX+gmrRfZMtntx6JsHolWubGXDopg=
github.com/calmh/go-sqlite3 v1.14.35-0.20260509063420-822b4765116d h1:liVtRMlDBqDyR4qKkMRIFTdUK6+aoG6Oh97DKnhrCHc=
github.com/calmh/go-sqlite3 v1.14.35-0.20260509063420-822b4765116d/go.mod h1:pjEuOr8IwzLJP2MfGeTb0A35jauH+C2kbHKBr7yXKVQ=
github.com/calmh/incontainer v1.0.0 h1:g2cTUtZuFGmMGX8GoykPkN1Judj2uw8/3/aEtq4Z/rg=
github.com/calmh/incontainer v1.0.0/go.mod h1:eOhqnw15c9X+4RNBe0W3HlUZFfX16O0EDsCOInTndHY=
github.com/calmh/xdr v1.2.0 h1:GaGSNH4ZDw9kNdYqle6+RcAENiaQ8/611Ok+jQbBEeU=
github.com/calmh/xdr v1.2.0/go.mod h1:vO5+lCx/8xz7Ekd/ZLf+xuy7c1x6FMO1pBJyjDebwyg=
github.com/ccding/go-stun v0.1.5 h1:qEM367nnezmj7dv+SdT52prv5x6HUTG3nlrjX5aitlo=
github.com/ccding/go-stun v0.1.5/go.mod h1:cCZjJ1J3WFSJV6Wj8Y9Di8JMTsEXh6uv2eNmLzKaUeM=
github.com/ccding/go-stun v0.1.6 h1:gWPd1V50Uv2RAnIlmWgoUxjdWvvXp2w1W99oGDtYvzQ=
github.com/ccding/go-stun v0.1.6/go.mod h1:cCZjJ1J3WFSJV6Wj8Y9Di8JMTsEXh6uv2eNmLzKaUeM=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/certifi/gocertifi v0.0.0-20210507211836-431795d63e8d h1:S2NE3iHSwP0XV47EEXL8mWmRdEfGscSJ+7EgePNgt0s=
@@ -57,10 +55,10 @@ github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nos
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/getsentry/raven-go v0.2.0 h1:no+xWJRb5ZI7eE8TWgIq1jLulQiIoLG0IfYxv5JYMGs=
github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ=
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo=
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
github.com/go-ldap/ldap/v3 v3.4.13 h1:+x1nG9h+MZN7h/lUi5Q3UZ0fJ1GyDQYbPvbuH38baDQ=
github.com/go-ldap/ldap/v3 v3.4.13/go.mod h1:LxsGZV6vbaK0sIvYfsv47rfh4ca0JXokCoKjZxsszv0=
github.com/go-asn1-ber/asn1-ber v1.5.8 h1:H9AZkK22UOmfX8J84ubyaZxKJZ3FMHVwn8swoMML7iQ=
github.com/go-asn1-ber/asn1-ber v1.5.8/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
github.com/go-ldap/ldap/v3 v3.4.14 h1:D6PYdEgsaVzsXyr6w/yDC06Ria4uUhWm+Rb+er8lfAs=
github.com/go-ldap/ldap/v3 v3.4.14/go.mod h1:S4eJUMUNjDkE0ZJtIZdybwyb03sGGLW6gxXT1Hs8VKA=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
@@ -130,8 +128,8 @@ github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4d
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/klauspost/compress v1.19.0 h1:sXLILfc9jV2QYWkzFOPWStmcUVH2RHEB1JCdY2oVvCQ=
github.com/klauspost/compress v1.19.0/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
@@ -149,6 +147,8 @@ github.com/maruel/panicparse/v2 v2.5.0/go.mod h1:DA2fDiBk63bKfBf4CVZP9gb4fuvzdPb
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mattn/go-sqlite3 v1.14.48 h1:7XHIgl0a8HwOaiK4E47ozLkST78rR9+OtNGx27D/TFs=
github.com/mattn/go-sqlite3 v1.14.48/go.mod h1:6JTjA44L93a0QCyJef5YvlPoKXntQPjzWv5gtm9sB6w=
github.com/maxbrunsfeld/counterfeiter/v6 v6.12.0 h1:aOeI7xAOVdK+R6xbVsZuU9HmCZYmQVmZgPf9xJUd2Sg=
github.com/maxbrunsfeld/counterfeiter/v6 v6.12.0/go.mod h1:0hZWbtfeCYUQeAQdPLUzETiBhUSns7O6LDj9vH88xKA=
github.com/maxmind/geoipupdate/v6 v6.1.0 h1:sdtTHzzQNJlXF5+fd/EoPTucRHyMonYt/Cok8xzzfqA=
@@ -179,8 +179,8 @@ github.com/oschwald/geoip2-golang v1.13.0 h1:Q44/Ldc703pasJeP5V9+aFSZFmBN7DKHbNs
github.com/oschwald/geoip2-golang v1.13.0/go.mod h1:P9zG+54KPEFOliZ29i7SeYZ/GM6tfEL+rgSn03hYuUo=
github.com/oschwald/maxminddb-golang v1.13.1 h1:G3wwjdN9JmIK2o/ermkHM+98oX5fS+k5MbwsmL4MRQE=
github.com/oschwald/maxminddb-golang v1.13.1/go.mod h1:K4pgV9N/GcK694KSTmVSDTODk4IsCNThNdTmnaBZ/F8=
github.com/pierrec/lz4/v4 v4.1.26 h1:GrpZw1gZttORinvzBdXPUXATeqlJjqUG/D87TKMnhjY=
github.com/pierrec/lz4/v4 v4.1.26/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4=
github.com/pierrec/lz4/v4 v4.1.27 h1:+PhzhWDrjRj89TH2sw43nE3+4+W8lSxIuQadEHZyjUk=
github.com/pierrec/lz4/v4 v4.1.27/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@@ -190,20 +190,22 @@ github.com/posener/complete v1.2.3 h1:NP0eAhjcjImqslEwo/1hq7gpajME0fTLTezBKDqfXq
github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
github.com/prometheus/client_golang v1.24.0 h1:5XStIklKuAtJSNpdD3s8XJj/Yv78IQmE1kbNk87JrAI=
github.com/prometheus/client_golang v1.24.0/go.mod h1:QcsNdotprC2nS4BTM2ucbcqxd2CeXTEa9jW7zHO9iDE=
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs=
github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA=
github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
github.com/prometheus/common v0.70.0 h1:bcpru3tWPVnxGnETLgOV5jbp/JRXgYEyv65CuBLAMMI=
github.com/prometheus/common v0.70.0/go.mod h1:S/SFasQmgGiYH6C81LKCtYa8QACgthGg5zxL2udV7SY=
github.com/prometheus/procfs v0.21.1 h1:GljZCt+zSTS+NZq88cyQ1LjZ+RCHp3uVuabBWA5+OJI=
github.com/prometheus/procfs v0.21.1/go.mod h1:aB55Cww9pdSJVHk0hUf0inxWyyjPogFIjmHKYgMKmtY=
github.com/puzpuzpuz/xsync/v3 v3.5.1 h1:GJYJZwO6IdxN/IKbneznS6yPkVC+c3zyY/j19c++5Fg=
github.com/puzpuzpuz/xsync/v3 v3.5.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw=
github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
github.com/rabbitmq/amqp091-go v1.11.0 h1:HxIctVm9Gid/Vtn706necmZ7Wj6pgGI2eqplRbEY8O8=
github.com/rabbitmq/amqp091-go v1.11.0/go.mod h1:Hy4jKW5kQART1u+JkDTF9YYOQUHXqMuhrgxOEeS7G4o=
github.com/quic-go/go-ossfuzz-seeds v0.1.0 h1:APacT+iIaNF6fd8AGEiN3bT/Jtkd2jz4v4TzM7MFjy0=
github.com/quic-go/go-ossfuzz-seeds v0.1.0/go.mod h1:3IOHRbJIc+L6YKMwfDtJAM9Vj9k0YY4muhuyUYk5tbk=
github.com/quic-go/quic-go v0.60.0 h1:xcQioE8OM66UQLeUMHltK1CCcOu3JbVB4JAQdDQSB+0=
github.com/quic-go/quic-go v0.60.0/go.mod h1:wpKpjmPpftl30sL6pFh7REVpjbcCVy4zt2vDyK1TuJk=
github.com/rabbitmq/amqp091-go v1.13.0 h1:L8NA1WtF76C6KA3LAoufjfLgbist/If1UQYcsOjtxXA=
github.com/rabbitmq/amqp091-go v1.13.0/go.mod h1:Hy4jKW5kQART1u+JkDTF9YYOQUHXqMuhrgxOEeS7G4o=
github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 h1:bsUq1dX0N8AOIL7EB/X911+m4EHsnWEHeJ0c+3TTBrg=
github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
@@ -216,8 +218,8 @@ github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sclevine/spec v1.4.0 h1:z/Q9idDcay5m5irkZ28M7PtQM4aOISzOpj4bUPkDee8=
github.com/sclevine/spec v1.4.0/go.mod h1:LvpgJaFyvQzRvc1kaDs0bulYwzC70PbiYjC4QnFHkOM=
github.com/shirou/gopsutil/v4 v4.26.4 h1:B4SXVbcwTyrocPHEmWBC4uCYr4Xcu3MK1TXqbprAOWY=
github.com/shirou/gopsutil/v4 v4.26.4/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ=
github.com/shirou/gopsutil/v4 v4.26.6 h1:Mzr/npDtQC/xpeEuQKHZt8Zo9CmPvhTj8nkR8w5TLDs=
github.com/shirou/gopsutil/v4 v4.26.6/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
@@ -258,20 +260,20 @@ go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o=
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI=
golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8=
golang.org/x/exp v0.0.0-20260508232706-74f9aab9d74a h1:+3jdDGGB8NGb1Zktc737jlt3/A5f6UlwSzmvqUuufxw=
golang.org/x/exp v0.0.0-20260508232706-74f9aab9d74a/go.mod h1:d2fgXJLVs4dYDHUk5lwMIfzRzSrWCfGZb0ZqeLa/Vcw=
golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw=
golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk=
golang.org/x/exp v0.0.0-20260718201538-764159d718ef h1:LkZ48HFgy/TvhTI0bcWkjgFkgLyKUwcTbDjS0DUjw+A=
golang.org/x/exp v0.0.0-20260718201538-764159d718ef/go.mod h1:EdfpwwqSu+0Li0mzskwHU6FWDV3t9Q+RZDo3QMUtL3Q=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4=
golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ=
golang.org/x/mod v0.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk=
golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@@ -280,13 +282,13 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w=
golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ=
golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE=
golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180926160741-c2ed4eda69e7/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -309,25 +311,25 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=
golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/telemetry v0.0.0-20260508192327-42602be52be6 h1:HjU6IWBiAgRIdAJ9/y1rwCn+UELEmwV+VsTLzj/W4sE=
golang.org/x/telemetry v0.0.0-20260508192327-42602be52be6/go.mod h1:Eqhaxk/wZsWEH8CRxLwj6xzEJbz7k1EFGqx7nyCoabE=
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/telemetry v0.0.0-20260708182218-49f421fb7959 h1:RJhm5l6Fo4rmEIcndxDllNhhf/fAx8qIm4t6A7vpm2A=
golang.org/x/telemetry v0.0.0-20260708182218-49f421fb7959/go.mod h1:LV7u5Oco+Z/g6XI7PqN+EUUUGGkEcmB1uj2ceI0fOVg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs=
golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY=
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8=
golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0=
golang.org/x/tools v0.48.0 h1:3+hClM1aLL5mjMKm5ovokw9epgRXPuu2tILgismM6RE=
golang.org/x/tools v0.48.0/go.mod h1:08xX0orndb/F7jJxGDicx061tyd5pcMto75YMAXr6lk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -358,30 +360,30 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
modernc.org/cc/v4 v4.27.3 h1:uNCgn37E5U09mTv1XgskEVUJ8ADKpmFMPxzGJ0TSo+U=
modernc.org/cc/v4 v4.27.3/go.mod h1:3YjcbCqhoTTHPycJDRl2WZKKFj0nwcOIPBfEZK0Hdk8=
modernc.org/ccgo/v4 v4.32.4 h1:L5OB8rpEX4ZsXEQwGozRfJyJSFHbbNVOoQ59DU9/KuU=
modernc.org/ccgo/v4 v4.32.4/go.mod h1:lY7f+fiTDHfcv6YlRgSkxYfhs+UvOEEzj49jAn2TOx0=
modernc.org/cc/v4 v4.29.0 h1:CXgwL8cvxmyzBQZzbSl/6xFtMCryb6u8IOqDci39cgc=
modernc.org/cc/v4 v4.29.0/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI=
modernc.org/ccgo/v4 v4.34.6 h1:sBgfIwyN0TQ9C5hwIeuqyeAKyMWnbvj2fvpF4L11uzU=
modernc.org/ccgo/v4 v4.34.6/go.mod h1:SZ8YcN9NG7XVsQYdm6jYBvi8PQP1qi+kqB6OhjqI3Fk=
modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM=
modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU=
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
modernc.org/gc/v3 v3.1.2 h1:ZtDCnhonXSZexk/AYsegNRV1lJGgaNZJuKjJSWKyEqo=
modernc.org/gc/v3 v3.1.2/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
modernc.org/gc/v3 v3.1.4 h1:2g65LGVSmFQrXeITAw97x7hCRvZFcyE1uDP+7Vng7JI=
modernc.org/gc/v3 v3.1.4/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
modernc.org/libc v1.72.0 h1:IEu559v9a0XWjw0DPoVKtXpO2qt5NVLAnFaBbjq+n8c=
modernc.org/libc v1.72.0/go.mod h1:tTU8DL8A+XLVkEY3x5E/tO7s2Q/q42EtnNWda/L5QhQ=
modernc.org/libc v1.74.1 h1:bdR4VTKFMC4966QSNZ05XLGI/VwzVa2kTUX51Dm0riQ=
modernc.org/libc v1.74.1/go.mod h1:uH4t5bOx3G3g9Xcmj10YKlTcVISlRDwv8VoQJG9n8Os=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
modernc.org/opt v0.2.0 h1:tGyef5ApycA7FSEOMraay9SaTk5zmbx7Tu+cJs4QKZg=
modernc.org/opt v0.2.0/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
modernc.org/sqlite v1.50.0 h1:eMowQSWLK0MeiQTdmz3lqoF5dqclujdlIKeJA11+7oM=
modernc.org/sqlite v1.50.0/go.mod h1:m0w8xhwYUVY3H6pSDwc3gkJ/irZT/0YEXwBlhaxQEew=
modernc.org/sqlite v1.54.0 h1:JCxR4qwkJvOaqAoYcgDoO25Nc+ROg6EJ2LfBVzdrgog=
modernc.org/sqlite v1.54.0/go.mod h1:4ntCLuNmnH8+GNqjka1wNg7KJd5/Hi5FYp8K+XQ7GZw=
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
+15 -15
View File
@@ -445,6 +445,20 @@ ul.three-columns li, ul.two-columns li {
-moz-column-count: 1;
column-count: 1;
}
/* All buttons, except panel headings, get bottom margin, as they
won't fit beside each other anymore. Reduce footer padding to
compensate for the margin. */
.btn:not(.panel-heading),
.btn:not(.panel-heading) + .btn:not(.panel-heading) {
margin-bottom: 10px;
}
.panel-footer {
padding-bottom: 0;
}
.modal-footer {
padding-bottom: 5px;
}
}
@media (max-width:479px) {
@@ -477,20 +491,6 @@ ul.three-columns li, ul.two-columns li {
width: 100%;
}
/* All buttons, except panel headings, get bottom margin, as they
won't fit beside each other anymore. Reduce footer padding to
compensate for the margin. */
.btn:not(.panel-heading),
.btn:not(.panel-heading) + .btn:not(.panel-heading) {
margin-bottom: 10px;
}
.panel-footer {
padding-bottom: 0;
}
.modal-footer {
padding-bottom: 5px;
}
table.table-auto td,
table.table-auto th,
table.table-condensed td,
@@ -552,4 +552,4 @@ html[lang|="ko"] i {
.select-on-click {
-webkit-user-select: all;
user-select: all;
}
}
+2
View File
@@ -125,6 +125,8 @@
"Discovery Status": "Discovery Status",
"Dismiss": "Dismiss",
"Do not add it to the ignore list, so this notification may recur.": "Do not add it to the ignore list, so this notification may recur.",
"Do not add it to the ignore list, so this notification will reappear if the device connects again.": "Do not add it to the ignore list, so this notification will reappear if the device connects again.",
"Do not add it to the ignore list, so this notification will reappear if the device offering this folder connects again.": "Do not add it to the ignore list, so this notification will reappear if the device offering this folder connects again.",
"Do not restore": "Do not restore",
"Do not restore all": "Do not restore all",
"Do you want to enable watching for changes for all your folders?": "Do you want to enable watching for changes for all your folders?",
+25 -25
View File
@@ -27,7 +27,7 @@
"Allowed Networks": "Redes permitidas",
"Alphabetic": "Alfabético",
"Altered by ignoring deletes.": "Alterado, ignorando eliminaciones.",
"Always turned on when the folder type is \"{%foldertype%}\".": "Siempre habilitado cuando el tipo de carpeta es \"{{foldertype}}\".",
"Always turned on when the folder type is \"{%foldertype%}\".": "Siempre habilitado cuando el tipo de carpeta es «{{foldertype}}».",
"An external command handles the versioning. It has to remove the file from the shared folder. If the path to the application contains spaces, it should be quoted.": "Un comando externo maneja las versiones. Tienes que eliminar el archivo de la carpeta compartida. Si la ruta a la aplicación contiene espacios, ésta debe estar entre comillas.",
"Anonymous Usage Reporting": "Informe de uso anónimo",
"Anonymous usage report format has changed. Would you like to move to the new format?": "El formato del informe de uso anónimo a cambiado. ¿Le gustaría pasar al nuevo formato?",
@@ -54,7 +54,7 @@
"Body:": "Contenido:",
"Bugs": "Errores",
"Cancel": "Cancelar",
"Cannot be enabled when the folder type is \"{%foldertype%}\".": "No se puede habilitar cuando el tipo de carpeta es \"{{foldertype}}\".",
"Cannot be enabled when the folder type is \"{%foldertype%}\".": "No se puede habilitar cuando el tipo de carpeta es «{{foldertype}}».",
"Changelog": "Registro de cambios",
"Clean out after": "Limpiar tras",
"Cleaning Versions": "Limpiando versiones",
@@ -72,7 +72,7 @@
"Connection Management": "Gestión de conexiones",
"Connection Type": "Tipo de conexión",
"Connections": "Conexiones",
"Connections via relays might be rate limited by the relay": "Las conexiones a través de res pueden estar limitadas por la velocidad del relé",
"Connections via relays might be rate limited by the relay": "Las conexiones a través de retransmisores pueden estar limitadas por la velocidad del servidor de retransmisión",
"Continuously watching for changes is now available within Syncthing. This will detect changes on disk and issue a scan on only the modified paths. The benefits are that changes are propagated quicker and that less full scans are required.": "Ahora está disponible en Syncthing el control de cambios continuo. Se detectarán los cambios en disco y se hará un escaneado sólo en las rutas modificadas. Los beneficios son que los cambios se propagan más rápido y que se requieren menos escaneos completos.",
"Copied from elsewhere": "Copiado de otro sitio",
"Copied from original": "Copiado del original",
@@ -98,7 +98,7 @@
"Deselect devices to stop sharing this folder with.": "Deselecciona dispositivos para dejar de compartir esta carpeta.",
"Deselect folders to stop sharing with this device.": "Deselecciona carpetas para dejar de compartir con este dispositivo.",
"Device": "Dispositivo",
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "El dispositivo \"{{name}}\" ({{device}} en la dirección {{address}}) quiere conectarse. Añadir nuevo dispositivo?",
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "El dispositivo «{{name}}» ({{device}} en la dirección {{address}}) quiere conectarse. ¿Añadir nuevo dispositivo?",
"Device Certificate": "Certificado del dispositivo",
"Device Group": "Grupo dispositivo",
"Device ID": "ID del dispositivo",
@@ -143,12 +143,12 @@
"Enable Relaying": "Habilitar retransmisión",
"Enabled": "Activado",
"Enables sending extended attributes to other devices, and applying incoming extended attributes. May require running with elevated privileges.": "Permite enviar atributos extendidos a otros dispositivos y aplicar atributos extendidos entrantes. Puede ser necesaria la ejecución con privilegios elevados.",
"Enables sending extended attributes to other devices, but not applying incoming extended attributes. This can have a significant performance impact. Always enabled when \"Sync Extended Attributes\" is enabled.": "Permite enviar atributos extendidos a otros dispositivos, pero no aplica los atributos extendidos entrantes. Puede tener impacto en el rendimiento. Siempre se habilita cuando \"Sincronizar atributos extendidos\" está habilitado.",
"Enables sending extended attributes to other devices, but not applying incoming extended attributes. This can have a significant performance impact. Always enabled when \"Sync Extended Attributes\" is enabled.": "Permite enviar atributos extendidos a otros dispositivos, pero no aplica los atributos extendidos entrantes. Puede tener impacto en el rendimiento. Siempre se habilita cuando «Sincronizar atributos extendidos» está habilitado.",
"Enables sending ownership information to other devices, and applying incoming ownership information. Typically requires running with elevated privileges.": "Permite enviar información de propietario a otros dispositivos y que se aplique la información de propietario recibida. Puede ser necesaria la ejecución con privilegios elevados.",
"Enables sending ownership information to other devices, but not applying incoming ownership information. This can have a significant performance impact. Always enabled when \"Sync Ownership\" is enabled.": "Permite enviar información de propietario a otros dispositivos, pero no aplicar la información de propiedad entrante. Puede tener impacto en el rendimiento. Siempre se habilita cuando \"Sincronizar propietario\" está habilitado.",
"Enter a non-negative number (e.g., \"2.35\") and select a unit. Percentages are as part of the total disk size.": "Introduce un número no negativo (por ejemplo, \"2.35\") y selecciona una unidad. Los porcentajes son como parte del tamaño total del disco.",
"Enables sending ownership information to other devices, but not applying incoming ownership information. This can have a significant performance impact. Always enabled when \"Sync Ownership\" is enabled.": "Permite enviar información de propietario a otros dispositivos, pero no aplicar la información de propiedad entrante. Puede tener impacto en el rendimiento. Siempre se habilita cuando «Sincronizar propietario» está habilitado.",
"Enter a non-negative number (e.g., \"2.35\") and select a unit. Percentages are as part of the total disk size.": "Introduce un número no negativo (por ejemplo, «2.35») y selecciona una unidad. Los porcentajes se calculan sobre el tamaño total del disco.",
"Enter a non-privileged port number (1024 - 65535).": "Introduce un puerto sin privilegios (1024 - 65535).",
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Introduce direcciones separadas por comas (\"tcp://ip:port\", \"tcp://host:port\") o \"dynamic\" para realizar el descubrimiento automático de la dirección.",
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Introduce direcciones separadas por comas («tcp://ip:port», «tcp://host:port») o «dynamic» para realizar el descubrimiento automático de la dirección.",
"Enter ignore patterns, one per line.": "Introduce patrones a ignorar, uno por línea.",
"Enter up to three octal digits.": "Introduce hasta tres dígitos octales.",
"Error": "Error",
@@ -177,8 +177,8 @@
"Folder Path": "Ruta de la carpeta",
"Folder Status": "Estado de la carpeta",
"Folder Type": "Tipo de carpeta",
"Folder type \"{%receiveEncrypted%}\" can only be set when adding a new folder.": "El tipo de carpeta \"{{receiveEncrypted}}\" sólo puede ser seleccionado al añadir una nueva carpeta.",
"Folder type \"{%receiveEncrypted%}\" cannot be changed after adding the folder. You need to remove the folder, delete or decrypt the data on disk, and add the folder again.": "El tipo de carpeta \"{{receiveEncrypted}}\" no se puede cambiar después de añadir la carpeta. Es necesario eliminar la carpeta, eliminar o descifrar los datos en el disco y volver a añadir la carpeta.",
"Folder type \"{%receiveEncrypted%}\" can only be set when adding a new folder.": "El tipo de carpeta «{{receiveEncrypted}}» solo puede ser seleccionado al añadir una nueva carpeta.",
"Folder type \"{%receiveEncrypted%}\" cannot be changed after adding the folder. You need to remove the folder, delete or decrypt the data on disk, and add the folder again.": "El tipo de carpeta «{{receiveEncrypted}}» no se puede cambiar después de añadir la carpeta. Es necesario eliminar la carpeta, eliminar o descifrar los datos en el disco y volver a añadir la carpeta.",
"Folders": "Carpetas",
"For the following folders an error occurred while starting to watch for changes. It will be retried every minute, so the errors might go away soon. If they persist, try to fix the underlying issue and ask for help if you can't.": "Se ha producido un error al comenzar a buscar cambios para las siguientes carpetas. Se hará un reintento cada minuto, por lo que los errores podrían desaparecer pronto. Si persisten trata de arreglar el problema subyacente o pide ayuda.",
"Forever": "Para siempre",
@@ -197,7 +197,7 @@
"Global Discovery Servers": "Servidores globales de descubrimiento",
"Global State": "Estado global",
"Help": "Ayuda",
"Hint: only deny-rules detected while the default is deny. Consider adding \"permit any\" as last rule.": "Sugerencia: sólo se detectan reglas para denegar y el valor predeterminado es denegar. Considera añadir \"permitir cualquiera\" como última regla.",
"Hint: only deny-rules detected while the default is deny. Consider adding \"permit any\" as last rule.": "Sugerencia: sólo se detectan reglas para denegar y el valor predeterminado es denegar. Considera añadir «permitir cualquiera» como última regla.",
"Home page": "Página de inicio",
"However, your current settings indicate you might not want it enabled. We have disabled automatic crash reporting for you.": "Sin embargo tu configuración actual indica que puedes no querer habilitarlo. Hemos desactivado los informes automáticos de fallos por tí.",
"Identification": "Identificador",
@@ -219,7 +219,7 @@
"Introduced By": "Presentado por",
"Introducer": "Presentador",
"Introduction": "Introducción",
"Inversion of the given condition (i.e. do not exclude)": "Inversión de la condición dada (por ejemplo, \"no excluir\")",
"Inversion of the given condition (i.e. do not exclude)": "Invertir la condición dada (por ejemplo «no excluir»)",
"Keep Versions": "Versiones a conservar",
"LDAP": "LDAP",
"Largest First": "Mayor tamaño primero",
@@ -324,8 +324,8 @@
"Received data is already encrypted": "Los datos recibidos ya están cifrados",
"Recent Changes": "Cambios recientes",
"Reduced by ignore patterns": "Reducido por patrones a ignorar",
"Relay LAN": "Re LAN",
"Relay WAN": "Re WAN",
"Relay LAN": "Retransmisión LAN",
"Relay WAN": "Retransmisión WAN",
"Release Notes": "Notas de la versión",
"Release candidates contain the latest features and fixes. They are similar to the traditional bi-weekly Syncthing releases.": "Las versiones candidatas contienen las últimas funcionalidades y correcciones. Son similares a las tradicionales versiones bisemanales de Syncthing.",
"Remote Devices": "Dispositivos Remotos",
@@ -400,7 +400,7 @@
"Statistics": "Estadísticas",
"Stay logged in": "Permanecer conectado",
"Stopped": "Detenido",
"Stores and syncs only encrypted data. Folders on all connected devices need to be set up with the same password or be of type \"{%receiveEncrypted%}\" too.": "Sólo almacena y sincroniza datos cifrados. Las carpetas de todos los dispositivos conectados deben estar configuradas con la misma contraseña o ser del tipo \"{{receiveEncrypted}}\".",
"Stores and syncs only encrypted data. Folders on all connected devices need to be set up with the same password or be of type \"{%receiveEncrypted%}\" too.": "Solo almacena y sincroniza datos cifrados. Las carpetas de todos los dispositivos conectados deben estar configuradas con la misma contraseña o ser del tipo «{{receiveEncrypted}}».",
"Subject:": "Asunto:",
"Support": "Soporte",
"Support Bundle": "Paquete de soporte",
@@ -409,7 +409,7 @@
"Sync Protocol Listen Addresses": "Direcciones de escucha del protocolo de sincronización",
"Sync Status": "Estado de la sincronización",
"Syncing": "Sincronizando",
"Syncthing device ID for \"{%devicename%}\"": "ID del dispositivo de sincronización para \"{{devicename}}\"",
"Syncthing device ID for \"{%devicename%}\"": "ID del dispositivo de sincronización para «{{devicename}}»",
"Syncthing has been shut down.": "Syncthing se ha detenido.",
"Syncthing includes the following software or portions thereof:": "Syncthing incluye el siguiente software o partes de él:",
"Syncthing is Free and Open Source Software licensed as MPL v2.0.": "Syncthing es software libre de código abierto con licencia MPL v2.0.",
@@ -432,7 +432,7 @@
"The cleanup interval cannot be blank.": "El intervalo de limpieza no puede estar vacío.",
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "La configuración ha sido guardada pero no activada. Syncthing debe reiniciarse para activar la nueva configuración.",
"The device ID cannot be blank.": "El ID del dispositivo no puede estar vacío.",
"The device ID to enter here can be found in the \"Actions > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "El ID del dispositivo a introducir aquí se puede encontrar en el diálogo \"Acciones > Mostrar ID\" del otro dispositivo. Los espacios y barras son opcionales (ignorados).",
"The device ID to enter here can be found in the \"Actions > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "El ID del dispositivo a introducir aquí se puede encontrar en el diálogo «Acciones > Mostrar ID» del otro dispositivo. Los espacios y barras son opcionales (ignorados).",
"The encrypted usage report is sent daily. It is used to track common platforms, folder sizes, and app versions. If the reported data set is changed you will be prompted with this dialog again.": "El informe encriptado de uso se envía diariamente. Se usa para rastrear plataformas comunes, tamaños de carpetas y versiones de la aplicación. Si el conjunto de datos enviados en el informes se cambia, se le pedirá a usted autorización de nuevo.",
"The entered device ID does not look valid. It should be a 52 or 56 character string consisting of letters and numbers, with spaces and dashes being optional.": "El ID del dispositivo introducido no parece válido. Debe ser una cadena de 52 ó 56 caracteres formada por letras y números, con espacios y guiones opcionales.",
"The folder ID cannot be blank.": "El ID de la carpeta no puede estar vacío.",
@@ -473,7 +473,7 @@
"This setting controls the free space required on the home (i.e., index database) disk.": "Determina el espacio libre necesario en el disco principal (con la. base de datos de índices).",
"Time": "Hora",
"Time the item was last modified": "Hora de última modificación del elemento",
"To connect with the Syncthing device named \"{%devicename%}\", add a new remote device on your end with this ID:": "Para conectarse con el dispositivo Syncthing llamado \"{{devicename}}\", añada un nuevo dispositivo remoto en su extremo con este ID:",
"To connect with the Syncthing device named \"{%devicename%}\", add a new remote device on your end with this ID:": "Para conectarse con el dispositivo Syncthing llamado «{{devicename}}», añada un nuevo dispositivo remoto en su extremo con este ID:",
"To permit a rule, have the checkbox checked. To deny a rule, leave it unchecked.": "Para permitir una regla, marca la casilla. Para denegar una regla, déjala sin marcar.",
"Today": "Hoy",
"Trash Can": "Papelera",
@@ -516,10 +516,10 @@
"Waiting to Scan": "Esperando para escanear",
"Waiting to Sync": "Esperando para sincronizar",
"Warning": "Atención",
"Warning, this path is a parent directory of an existing folder \"{%otherFolder%}\".": "Atención, esta ruta es un directorio principal de la carpeta ya existente \"{{otherFolder}}\".",
"Warning, this path is a parent directory of an existing folder \"{%otherFolderLabel%}\" ({%otherFolder%}).": "Atención, esta ruta es una carpeta principal de una carpeta existente \"{{otherFolderLabel}}\" ({{otherFolder}}).",
"Warning, this path is a subdirectory of an existing folder \"{%otherFolder%}\".": "Atención, esta ruta es un subdirectorio de una carpeta ya existente llamada \"{{otherFolder}}\".",
"Warning, this path is a subdirectory of an existing folder \"{%otherFolderLabel%}\" ({%otherFolder%}).": "Atención, esta ruta es un subdirectorio de una carpeta ya existente llamada \"{{otherFolderLabel}}\" ({{otherFolder}}).",
"Warning, this path is a parent directory of an existing folder \"{%otherFolder%}\".": "Atención, esta ruta es un directorio principal de la carpeta ya existente «{{otherFolder}}».",
"Warning, this path is a parent directory of an existing folder \"{%otherFolderLabel%}\" ({%otherFolder%}).": "Atención, esta ruta es una carpeta principal de una carpeta existente «{{otherFolderLabel}}» ({{otherFolder}}).",
"Warning, this path is a subdirectory of an existing folder \"{%otherFolder%}\".": "Atención, esta ruta es un subdirectorio de una carpeta ya existente llamada «{{otherFolder}}».",
"Warning, this path is a subdirectory of an existing folder \"{%otherFolderLabel%}\" ({%otherFolder%}).": "Atención, esta ruta es un subdirectorio de una carpeta ya existente llamada «{{otherFolderLabel}}» ({{otherFolder}}).",
"Warning: If you are using an external watcher like {%syncthingInotify%}, you should make sure it is deactivated.": "Atención: si estás utilizando un observador externo como {{syncthingInotify}}, debes asegurarte de que está desactivado.",
"Watch for Changes": "Detectar cambios",
"Watching for Changes": "Detectando cambios",
@@ -537,7 +537,7 @@
"You have no ignored folders.": "No hay carpetas ignoradas.",
"You have unsaved changes. Do you really want to discard them?": "Hay cambios sin guardar. ¿Estás seguro de que quieres descartarlos?",
"You must keep at least one version.": "Debes conservar al menos una versión.",
"You should never add or change anything locally in a \"{%receiveEncrypted%}\" folder.": "Nunca debes añadir o cambiar nada localmente en una carpeta \"{{receiveEncrypted}}\".",
"You should never add or change anything locally in a \"{%receiveEncrypted%}\" folder.": "Nunca debes añadir o cambiar nada localmente en una carpeta «{{receiveEncrypted}}».",
"Your SMS app should open to let you choose the recipient and send it from your own number.": "Tu aplicación de SMS debería abrirse para permitirte elegir el destinatario y enviarlo desde tu propio número.",
"Your email app should open to let you choose the recipient and send it from your own address.": "Tu aplicación de email debería abrirse para permitirte elegir el destinatario y enviarlo desde tu propia dirección.",
"days": "días",
@@ -561,7 +561,7 @@
}
},
"unknown device": "dispositivo desconocido",
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} quiere compartir la carpeta \"{{folder}}\".",
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} quiere compartir la carpeta \"{{folderlabel}}\" ({{folder}}).",
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} quiere compartir la carpeta «{{folder}}».",
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} quiere compartir la carpeta «{{folderlabel}}» ({{folder}}).",
"{%reintroducer%} might reintroduce this device.": "{{reintroducer}} puede presentar de nuevo este dispositivo."
}
+2 -2
View File
@@ -124,7 +124,7 @@
"Enter a non-negative number (e.g., \"2.35\") and select a unit. Percentages are as part of the total disk size.": "Negatiboa ez den zenbaki bat hauta ezazu (\"2.35\" adib.) bai eta unitate bat. Disko osoaren ehuneko espazioa",
"Enter a non-privileged port number (1024 - 65535).": "Abantailatua ez den portu zenbalki bat sar ezazu (1024 - 65535)",
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Sartu komaz bereizitako helbideak (\"tcp://ip:port\", \"tcp://host:port\"), edo \"dynamic\" helbidea automatikoki bilatzeko.",
"Enter ignore patterns, one per line.": "Ezkluzio filtroak sar, lerro bakoitzean bat bakarrik.",
"Enter ignore patterns, one per line.": "Sartu baztertze-patroiak, bakarra lerro bakoitzeko.",
"Enter up to three octal digits.": "Sartu 3 digitu bitarte",
"Error": "Hutsa",
"External File Versioning": "Fitxategi bertsioen kanpoko kudeaketa",
@@ -229,7 +229,7 @@
"Optional descriptive label for the folder. Can be different on each device.": "Partekatzearen izen hautuzkoa eta atsegina, zure gisa. Tresna bakotxean desberdina izaiten ahal da.",
"Options": "Hautuzkoak",
"Out of Sync": "Ez sinkronizatua",
"Out of Sync Items": "Ez sinkronizatu elementuak",
"Out of Sync Items": "Sinkronizatu gabeko elementuak",
"Outgoing Rate Limit (KiB/s)": "Bidaltze emari gehienekoa (KiB/s)",
"Override": "Gainidatzi",
"Override Changes": "Aldaketak desegin",
+6
View File
@@ -11,6 +11,7 @@
"Add Device": "Dodaj uređaj",
"Add Folder": "Dodaj mapu",
"Add Remote Device": "Dodaj udaljeni uređaj",
"Add devices from the introducer to our device list, for mutually shared folders.": "Dodaj uređaje od posrednika u našu listu uređaja, za međusobno dijeljene mape.",
"Add filter entry": "Dodaj unos filtra",
"Add ignore patterns": "Dodaj uzorke zanemarivanja",
"Add new folder?": "Dodati novu mapu?",
@@ -187,6 +188,7 @@
"GUI Authentication Password": "Lozinka za GUI autentifikacju",
"GUI Authentication User": "Korisnik za GUI autentifikacju",
"GUI Authentication: Set User and Password": "GUI autentifikacija: Postavite korisnika i lozinku",
"GUI Listen Address": "GUI adresa za slušanje",
"GUI Override Directory": "Direktorij za nadjačavanje GUI-ja",
"GUI Theme": "Tema GUI-ja",
"General": "Općenito",
@@ -214,6 +216,8 @@
"Incorrect user name or password.": "Neispravno korisničko ime ili lozinka.",
"Info": "Informacije",
"Internally used paths:": "Interno korištene staze:",
"Introduced By": "Uveden od",
"Introducer": "Posrednik",
"Introduction": "Uvod",
"Inversion of the given condition (i.e. do not exclude)": "Inverzija zadanog uvjeta (tj. ne isključuj)",
"Keep Versions": "Zadrži verzije",
@@ -248,6 +252,7 @@
"Log tailing paused. Scroll to the bottom to continue.": "Praćenje zapisa je pauzirano. Pomaknite se na dno kako biste nastavili.",
"Login failed, see Syncthing logs for details.": "Prijava nije uspjela. Pogledaj detalje u odjeljku zapisima sinkronizacije.",
"Logs": "Zapisi",
"Maintain an index of all blocks in the folder, enabling reuse of blocks from other files when syncing changes. Disable to reduce database size at the cost of not being able to reuse blocks across files.": "Održavaj indeks svih blokova u mapi, što omogućava ponovnu upotrebu blokova iz drugih datoteka pri sinkronizaciji promjena. Onemogući da smanjiš veličinu baze podataka, uz cijenu nemogućnosti ponovne upotrebe blokova između datoteka.",
"Major Upgrade": "Velika nadogradnja",
"Mass actions": "Masovne radnje",
"Maximum Age": "Maksimalna starost",
@@ -401,6 +406,7 @@
"Support Bundle": "Paket podrške",
"Sync Extended Attributes": "Sinkroniziraj proširena svojstva",
"Sync Ownership": "Sinkroniziraj vlasništvo",
"Sync Protocol Listen Addresses": "Adrese za slušanje sinkronizacijskog protokola",
"Sync Status": "Stanje sinkronizacije",
"Syncing": "Sinkroniziranje",
"Syncthing device ID for \"{%devicename%}\"": "Syncthing ID uređaja za „{{devicename}}“",
+7
View File
@@ -50,6 +50,7 @@
"Automatically create or share folders that this device advertises at the default path.": "Crea o condividi automaticamente le cartelle che questo dispositivo presenta sul percorso predefinito.",
"Available debug logging facilities:": "Servizi di debug disponibili:",
"Be careful!": "Fai attenzione!",
"Block Indexing": "Indicizzazione a blocchi",
"Body:": "Corpo:",
"Bugs": "Bug",
"Cancel": "Annulla",
@@ -99,6 +100,7 @@
"Device": "Dispositivo",
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "Il dispositivo \"{{name}}\" ({{device}} - {{address}}) chiede di connettersi. Aggiungere il nuovo dispositivo?",
"Device Certificate": "Certificato del dispositivo",
"Device Group": "Gruppo del Dispositivo",
"Device ID": "ID Dispositivo",
"Device Identification": "Identificazione Dispositivo",
"Device Name": "Nome Dispositivo",
@@ -169,6 +171,7 @@
"Filter by date": "Filtra per data",
"Filter by name": "Filtra per nome",
"Folder": "Cartella",
"Folder Group": "Gruppo della Cartella",
"Folder ID": "ID Cartella",
"Folder Label": "Etichetta per la Cartella",
"Folder Path": "Percorso Cartella",
@@ -249,6 +252,7 @@
"Log tailing paused. Scroll to the bottom to continue.": "Visualizzazione log in pausa. Scorri fino in fondo per continuare.",
"Login failed, see Syncthing logs for details.": "Accesso non riuscito, vedi i log di Syncthing per i dettagli.",
"Logs": "Log",
"Maintain an index of all blocks in the folder, enabling reuse of blocks from other files when syncing changes. Disable to reduce database size at the cost of not being able to reuse blocks across files.": "Mantiene un indice di tutti i blocchi nella cartella, consentendo il riutilizzo dei blocchi da altri file durante la sincronizzazione delle modifiche. Disabilitare questa opzione per ridurre le dimensioni del database, a costo di non poter riutilizzare i blocchi tra i file.",
"Major Upgrade": "Aggiornamento Principale",
"Mass actions": "Azioni di massa",
"Maximum Age": "Durata Massima",
@@ -279,6 +283,8 @@
"Off": "Disattiva",
"Oldest First": "Prima il Meno Recente",
"Optional descriptive label for the folder. Can be different on each device.": "Etichetta descrittiva facoltativa della cartella. Può essere diversa su ogni dispositivo.",
"Optional group for the device. Can be different on each device.": "Gruppo opzionale per il dispositivo. Può essere diverso per ogni dispositivo.",
"Optional group for the folder. Can be different on each device.": "Gruppo opzionale per la cartella. Può essere diverso su ciascun dispositivo.",
"Options": "Opzioni",
"Out of Sync": "Non sincronizzato",
"Out of Sync Items": "Elementi Non Sincronizzati",
@@ -390,6 +396,7 @@
"Staggered": "Sfalsato",
"Staggered File Versioning": "Controllo Versione Cadenzato",
"Start Browser": "Avvia Browser",
"Starting": "In avvio",
"Statistics": "Statistiche",
"Stay logged in": "Resta connesso",
"Stopped": "Fermato",
+14 -8
View File
@@ -50,6 +50,7 @@
"Automatically create or share folders that this device advertises at the default path.": "Автоматически создавать, используя путь по умолчанию, или делиться папками, о которых сообщает это устройство.",
"Available debug logging facilities:": "Доступные средства отладочного журнала:",
"Be careful!": "Будьте осторожны!",
"Block Indexing": "Блочная индексация",
"Body:": "Тело:",
"Bugs": "Ошибки",
"Cancel": "Отмена",
@@ -99,6 +100,7 @@
"Device": "Устройство",
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "Устройство «{{name}}» ({{device}} на {{address}}) хочет подключиться. Добавить новое устройство?",
"Device Certificate": "Сертификат устройства",
"Device Group": "Группа устройства",
"Device ID": "ID устройства",
"Device Identification": "Идентификация устройства",
"Device Name": "Имя устройства",
@@ -132,9 +134,9 @@
"Downloading": "Загрузка",
"Edit": "Редактировать",
"Edit Device": "Редактирование устройства",
"Edit Device Defaults": "Изменить умолчания устройства",
"Edit Device Defaults": "Изменить настройки устройств",
"Edit Folder": "Редактирование папки",
"Edit Folder Defaults": "Изменить умолчания папки",
"Edit Folder Defaults": "Изменить настройки папок",
"Editing {%path%}.": "Правка {{path}}.",
"Enable Crash Reporting": "Включить отчёты о сбоях",
"Enable NAT traversal": "Использовать обход NAT",
@@ -169,8 +171,9 @@
"Filter by date": "Отфильтровать по дате",
"Filter by name": "Отфильтровать по имени",
"Folder": "Папка",
"Folder Group": "Группа папки",
"Folder ID": "ID папки",
"Folder Label": "Ярлык папки",
"Folder Label": "Название папки",
"Folder Path": "Путь к папке",
"Folder Status": "Статус папки",
"Folder Type": "Тип папки",
@@ -182,8 +185,8 @@
"Full Rescan Interval (s)": "Интервал полного сканирования (в секундах)",
"GUI": "Интерфейс",
"GUI / API HTTPS Certificate": "HTTPS-сертификат GUI/API",
"GUI Authentication Password": "Пароль для доступа к панели управления",
"GUI Authentication User": "Имя пользователя для доступа к панели управления",
"GUI Authentication Password": "Пароль",
"GUI Authentication User": "Имя пользователя",
"GUI Authentication: Set User and Password": "GUI аутентификация: Установите имя пользователя и пароль",
"GUI Listen Address": "Адрес GUI",
"GUI Override Directory": "Каталог переопределения графического интерфейса",
@@ -249,6 +252,7 @@
"Log tailing paused. Scroll to the bottom to continue.": "Вывод журнала приостановлен. Прокрутите вниз, чтобы продолжить.",
"Login failed, see Syncthing logs for details.": "Не удалось войти в систему. Посмотреть подробности можно в журнале Syncthing.",
"Logs": "Журналы",
"Maintain an index of all blocks in the folder, enabling reuse of blocks from other files when syncing changes. Disable to reduce database size at the cost of not being able to reuse blocks across files.": "Использовать индекс всех блоков в папке. Это позволит повторно использовать блоки из других файлов при синхронизации изменений. Отключите эту функцию, чтобы уменьшить размер базы данных. При этом вы не сможете повторно использовать блоки из разных файлов.",
"Major Upgrade": "Обновление основной версии",
"Mass actions": "Массовые действия",
"Maximum Age": "Максимальный срок",
@@ -271,14 +275,16 @@
"No File Versioning": "Без управления версиями файлов",
"No files will be deleted as a result of this operation.": "В результате этой операции никакие файлы не будут удалены.",
"No rules set": "Правила не заданы",
"No upgrades": "Нет обновлений",
"No upgrades": "Выключено",
"Not shared": "Нет общего доступа",
"Notice": "Внимание",
"Number of Connections": "Количество подключений",
"OK": "ОК",
"Off": "Отключить",
"Oldest First": "Сначала старые",
"Optional descriptive label for the folder. Can be different on each device.": "Необязательное описательное название папки. Может различаться на разных устройствах.",
"Optional descriptive label for the folder. Can be different on each device.": "Необязательное название папки. Может различаться на разных устройствах.",
"Optional group for the device. Can be different on each device.": "Необязательная группа для устройства. Может отличаться для каждого устройства.",
"Optional group for the folder. Can be different on each device.": "Необязательная группа для папки. Может отличаться на каждом устройстве.",
"Options": "Настройки",
"Out of Sync": "Нет синхронизации",
"Out of Sync Items": "Несинхронизированные элементы",
@@ -398,7 +404,7 @@
"Support": "Поддержка",
"Support Bundle": "Данные для поддержки",
"Sync Extended Attributes": "Синхронизировать расширенные атрибуты",
"Sync Ownership": "Синхронизация владений",
"Sync Ownership": "Синхронизировать владельца",
"Sync Protocol Listen Addresses": "Адрес протокола синхронизации",
"Sync Status": "Состояние синхронизации",
"Syncing": "Синхронизация",
+160 -160
View File
@@ -1,7 +1,7 @@
{
"A device with that ID is already added.": "En enhet med detta ID har redan lagts till.",
"A negative number of days doesn't make sense.": "Ett negativt antal dagar är inte rimligt.",
"A new major version may not be compatible with previous versions.": "En ny huvudversion kan eventuellt vara inkompatibel med tidigare versioner.",
"A new major version may not be compatible with previous versions.": "En ny huvudversion kanske inte är kompatibel med tidigare versioner.",
"API Key": "API-nyckel",
"About": "Om",
"Action": "Åtgärd",
@@ -15,22 +15,22 @@
"Add filter entry": "Lägg till filterregel",
"Add ignore patterns": "Lägg till ignoreringsmönster",
"Add new folder?": "Lägg till ny mapp?",
"Additionally the full rescan interval will be increased (times 60, i.e. new default of 1h). You can also configure it manually for every folder later after choosing No.": "Dessutom kommer det fullständiga återkommande skanningsintervallet att höjas (60 gånger, d.v.s. ny standard på 1h). Du kan också konfigurera den manuellt för varje mapp senare efter att du har valt Nej.",
"Additionally the full rescan interval will be increased (times 60, i.e. new default of 1h). You can also configure it manually for every folder later after choosing No.": "Dessutom förlängs intervallet för fullständig omskanning 60 gånger, till det nya standardvärdet 1 timme. Du kan också konfigurera det manuellt för varje mapp senare efter att du har valt Nej.",
"Address": "Adress",
"Addresses": "Adresser",
"Advanced": "Avancerat",
"Advanced Configuration": "Avancerad konfiguration",
"All Data": "Alla data",
"All Time": "All tid",
"All folders shared with this device must be protected by a password, such that all sent data is unreadable without the given password.": "Alla mappar som delas med denna enhet måste skyddas av ett lösenord, så att alla skickade data är oläsliga utan det angivna lösenordet.",
"Allow Anonymous Usage Reporting?": "Tillåt anonym användarstatistiksrapportering?",
"All Time": "Hela perioden",
"All folders shared with this device must be protected by a password, such that all sent data is unreadable without the given password.": "Alla mappar som delas med den här enheten måste skyddas av ett lösenord, så att alla skickade data är oläsliga utan det angivna lösenordet.",
"Allow Anonymous Usage Reporting?": "Tillåt anonym användningsrapportering?",
"Allowed Networks": "Tillåtna nätverk",
"Alphabetic": "Alfabetisk",
"Altered by ignoring deletes.": "Ändrad genom att ignorera borttagningar.",
"Altered by ignoring deletes.": "Ändrad eftersom borttagningar ignoreras.",
"Always turned on when the folder type is \"{%foldertype%}\".": "Alltid på när mapptypen är \"{{foldertype}}\".",
"An external command handles the versioning. It has to remove the file from the shared folder. If the path to the application contains spaces, it should be quoted.": "Ett externt kommando hanterar versionen. Det måste ta bort filen från den delade mappen. Om sökvägen till applikationen innehåller mellanslag bör den citeras.",
"Anonymous Usage Reporting": "Anonym användarstatistiksrapportering",
"Anonymous usage report format has changed. Would you like to move to the new format?": "Anonymt användningsrapportformat har ändrats. Vill du flytta till det nya formatet?",
"An external command handles the versioning. It has to remove the file from the shared folder. If the path to the application contains spaces, it should be quoted.": "Ett externt kommando hanterar versionshanteringen. Det måste ta bort filen från den delade mappen. Om sökvägen till programmet innehåller mellanslag ska den omges av citattecken.",
"Anonymous Usage Reporting": "Anonym användningsrapportering",
"Anonymous usage report format has changed. Would you like to move to the new format?": "Formatet för anonym användningsrapportering har ändrats. Vill du byta till det nya formatet?",
"Applied to LAN": "Tillämpas på LAN",
"Apply": "Tillämpa",
"Are you sure you want to override all remote changes?": "Är du säker på att du vill åsidosätta alla fjärrändringar?",
@@ -46,18 +46,18 @@
"Automatic Crash Reporting": "Automatisk kraschrapportering",
"Automatic upgrade now offers the choice between stable releases and release candidates.": "Automatisk uppgradering erbjuder nu valet mellan stabila utgåvor och utgåvskandidater.",
"Automatic upgrades": "Automatiska uppgraderingar",
"Automatic upgrades are always enabled for candidate releases.": "Automatiska uppgraderingar är alltid aktiverade för kandidatutgåvor.",
"Automatically create or share folders that this device advertises at the default path.": "Skapa eller dela automatiskt mappar som denna enhet annonserar på standardsökvägen.",
"Available debug logging facilities:": "Tillgängliga felsökningsfunktioner:",
"Be careful!": "Var aktsam!",
"Automatic upgrades are always enabled for candidate releases.": "Automatiska uppgraderingar är alltid aktiverade för utgåvskandidater.",
"Automatically create or share folders that this device advertises at the default path.": "Skapa eller dela automatiskt mappar som enheten erbjuder, med standardsökvägen.",
"Available debug logging facilities:": "Tillgängliga funktioner för felsökningsloggning:",
"Be careful!": "Var försiktig!",
"Block Indexing": "Blockindexering",
"Body:": "Meddelande:",
"Bugs": "Felrapporter",
"Cancel": "Avbryt",
"Cannot be enabled when the folder type is \"{%foldertype%}\".": "Kan inte aktiveras när mapptypen är \"{{foldertype}}\".",
"Changelog": "Ändringslogg",
"Clean out after": "Rensa efteråt",
"Cleaning Versions": "Rensningsversioner",
"Clean out after": "Rensa efter",
"Cleaning Versions": "Rensar versioner",
"Cleanup Interval": "Rensningsintervall",
"Click to see full identification string and QR code.": "Klicka för att se fullständig identifieringssträng och QR-kod.",
"Close": "Stäng",
@@ -68,12 +68,12 @@
"Configuration File": "Konfigurationsfil",
"Configured": "Konfigurerad",
"Connected (Unused)": "Ansluten (oanvänd)",
"Connection Error": "Anslutningsproblem",
"Connection Error": "Anslutningsfel",
"Connection Management": "Anslutningshantering",
"Connection Type": "Anslutningstyp",
"Connections": "Anslutningar",
"Connections via relays might be rate limited by the relay": "Anslutningar via reläer kan begränsas av reläen",
"Continuously watching for changes is now available within Syncthing. This will detect changes on disk and issue a scan on only the modified paths. The benefits are that changes are propagated quicker and that less full scans are required.": "Kontinuerlig bevakning av ändringar är nu tillgängligt i Syncthing. Detta kommer att upptäcka ändringar på disken och utfärda en skanning på endast de ändrade sökvägarna. Fördelarna är att ändringar delas snabbare och att mindre fullständiga skanningar krävs.",
"Connections via relays might be rate limited by the relay": "Anslutningar via reläer kan hastighetsbegränsas av reläet",
"Continuously watching for changes is now available within Syncthing. This will detect changes on disk and issue a scan on only the modified paths. The benefits are that changes are propagated quicker and that less full scans are required.": "Kontinuerlig bevakning av ändringar är nu tillgänglig i Syncthing. Ändringar på disken upptäcks och endast de ändrade sökvägarna skannas. Det gör att ändringar sprids snabbare och att färre fullständiga skanningar behövs.",
"Copied from elsewhere": "Kopierat från annanstans",
"Copied from original": "Kopierat från original",
"Copied!": "Kopierat!",
@@ -89,32 +89,32 @@
"Default Configuration": "Standardkonfiguration",
"Default Device": "Standardenhet",
"Default Folder": "Standardmapp",
"Default Ignore Patterns": "Standard ignoreringsmönster",
"Default Ignore Patterns": "Standardignoreringsmönster",
"Defaults": "Standard",
"Delete": "Ta bort",
"Delete Unexpected Items": "Ta bort oväntade objekt",
"Deleted {%file%}": "Tog bort {{file}}",
"Deselect All": "Avmarkera alla",
"Deselect devices to stop sharing this folder with.": "Avmarkera enheter som du vill sluta dela denna mapp med.",
"Deselect folders to stop sharing with this device.": "Avmarkera mappar för att sluta dela med denna enhet.",
"Deselect devices to stop sharing this folder with.": "Avmarkera de enheter som mappen inte längre ska delas med.",
"Deselect folders to stop sharing with this device.": "Avmarkera de mappar som inte längre ska delas med den här enheten.",
"Device": "Enhet",
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "Enhet \"{{name}}\" ({{device}} på {{address}}) vill ansluta. Lägg till ny enhet?",
"Device Certificate": "Enhetscertifikat",
"Device Group": "Enhetsgrupp",
"Device ID": "Enhets-ID",
"Device Identification": "Enhetens identifikation",
"Device Identification": "Enhetsidentifiering",
"Device Name": "Enhetsnamn",
"Device Status": "Enhetsstatus",
"Device is untrusted, enter encryption password": "Enheten är otillförlitlig, ange krypteringslösenord",
"Device rate limits": "Enhetshastighetsgränser",
"Device is untrusted, enter encryption password": "Enheten är inte betrodd. Ange krypteringslösenordet.",
"Device rate limits": "Hastighetsgränser för enheten",
"Device that last modified the item": "Enhet som senast ändrade objektet",
"Devices": "Enheter",
"Disable Crash Reporting": "Inaktivera kraschrapportering",
"Disabled": "Inaktiverad",
"Disabled periodic scanning and disabled watching for changes": "Inaktiverad periodisk skanning och inaktiverad bevakning av ändringar",
"Disabled periodic scanning and enabled watching for changes": "Inaktiverad periodisk skanning och aktiverad bevakning av ändringar",
"Disabled periodic scanning and failed setting up watching for changes, retrying every 1m:": "Inaktiverad periodisk skanning och misslyckades med att ställa in bevakning av ändringar, försöker igen var 1:e minut:",
"Disables comparing and syncing file permissions. Useful on systems with nonexistent or custom permissions (e.g. FAT, exFAT, Synology, Android).": "Inaktiverar att jämföra och synkronisera filbehörigheter. Användbart för system med obefintliga eller anpassade behörigheter (t.ex. FAT, exFAT, Synology, Android).",
"Disabled periodic scanning and disabled watching for changes": "Periodisk skanning och bevakning av ändringar är inaktiverade",
"Disabled periodic scanning and enabled watching for changes": "Periodisk skanning är inaktiverad och bevakning av ändringar är aktiverad",
"Disabled periodic scanning and failed setting up watching for changes, retrying every 1m:": "Periodisk skanning är inaktiverad och bevakning av ändringar kunde inte konfigureras. Ett nytt försök görs varje minut:",
"Disables comparing and syncing file permissions. Useful on systems with nonexistent or custom permissions (e.g. FAT, exFAT, Synology, Android).": "Inaktiverar jämförelse och synkronisering av filbehörigheter. Användbart system utan behörigheter eller med anpassade behörigheter, exempelvis FAT, exFAT, Synology och Android.",
"Discard": "Kassera",
"Disconnected": "Frånkopplad",
"Disconnected (Inactive)": "Frånkopplad (inaktiv)",
@@ -131,43 +131,43 @@
"Documentation": "Dokumentation",
"Download Rate": "Hämtningshastighet",
"Downloaded": "Hämtat",
"Downloading": "Tar emot",
"Downloading": "Hämtar",
"Edit": "Redigera",
"Edit Device": "Redigera enhet",
"Edit Device Defaults": "Redigera enhetsstandard",
"Edit Device Defaults": "Redigera standardvärden för enheter",
"Edit Folder": "Redigera mapp",
"Edit Folder Defaults": "Redigera mappstandard",
"Edit Folder Defaults": "Redigera standardvärden för mappar",
"Editing {%path%}.": "Redigerar {{path}}.",
"Enable Crash Reporting": "Aktivera kraschrapportering",
"Enable NAT traversal": "Aktivera NAT-genompassering",
"Enable Relaying": "Aktivera vidarebefordring",
"Enable NAT traversal": "Aktivera NAT-traversering",
"Enable Relaying": "Aktivera reläförmedling",
"Enabled": "Aktiverad",
"Enables sending extended attributes to other devices, and applying incoming extended attributes. May require running with elevated privileges.": "Möjliggör sändning av utökade attribut till andra enheter, och tillämpning av inkommande utökade attribut. Kan kräva körning med förhöjda behörigheter.",
"Enables sending extended attributes to other devices, but not applying incoming extended attributes. This can have a significant performance impact. Always enabled when \"Sync Extended Attributes\" is enabled.": "Möjliggör sändning av utökade attribut till andra enheter, men inte att tillämpa inkommande utökade attribut. Detta kan ha en betydande inverkan på prestanda. Alltid aktiverad när \"Synkronisera utökade attribut\" är aktiverat.",
"Enables sending ownership information to other devices, and applying incoming ownership information. Typically requires running with elevated privileges.": "Möjliggör att skicka ägarinformation till andra enheter och tillämpa inkommande ägarinformation. Kräver vanligtvis körning med förhöjda behörigheter.",
"Enables sending ownership information to other devices, but not applying incoming ownership information. This can have a significant performance impact. Always enabled when \"Sync Ownership\" is enabled.": "Möjliggör att skicka ägarinformation till andra enheter, men inte tillämpa inkommande ägarinformation. Detta kan ha en betydande inverkan på prestanda. Alltid aktiverat när \"Synkronisera ägande\" är aktiverat.",
"Enter a non-negative number (e.g., \"2.35\") and select a unit. Percentages are as part of the total disk size.": "Ange ett icke-negativt antal (t.ex., \"2.35\") och välj en enhet. Procenttalen är som en del av den totala diskstorleken.",
"Enter a non-privileged port number (1024 - 65535).": "Ange ett icke-privilegierat portnummer (1024 - 65535).",
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Ange kommaseparerade (\"tcp://ip:port\", \"tcp://host:port\") adresser eller \"dynamic\" för att utföra automatisk upptäckt av adressen.",
"Enables sending extended attributes to other devices, and applying incoming extended attributes. May require running with elevated privileges.": "Gör det möjligt att skicka utökade attribut till andra enheter och tillämpa inkommande utökade attribut. Kan kräva körning med utökade behörigheter.",
"Enables sending extended attributes to other devices, but not applying incoming extended attributes. This can have a significant performance impact. Always enabled when \"Sync Extended Attributes\" is enabled.": "Gör det möjligt att skicka utökade attribut till andra enheter utan att tillämpa inkommande utökade attribut. Det kan verka prestandan avsevärt. Funktionen är alltid aktiverad när Synkronisera utökade attribut är aktiverat.",
"Enables sending ownership information to other devices, and applying incoming ownership information. Typically requires running with elevated privileges.": "Gör det möjligt att skicka ägarinformation till andra enheter och tillämpa inkommande ägarinformation. Kräver vanligtvis körning med utökade behörigheter.",
"Enables sending ownership information to other devices, but not applying incoming ownership information. This can have a significant performance impact. Always enabled when \"Sync Ownership\" is enabled.": "Gör det möjligt att skicka ägarinformation till andra enheter utan att tillämpa inkommande ägarinformation. Det kan verka prestandan avsevärt. Funktionen är alltid aktiverad när Synkronisera ägarskap” är aktiverat.",
"Enter a non-negative number (e.g., \"2.35\") and select a unit. Percentages are as part of the total disk size.": "Ange ett icke-negativt tal, t.ex. \"2.35\", och välj en enhet. Procenttal räknas som en andel av den totala diskstorleken.",
"Enter a non-privileged port number (1024 - 65535).": "Ange ett oprivilegierat portnummer (102465535).",
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Ange kommaseparerade adresser (\"tcp://ip:port\", \"tcp://host:port\") eller \"dynamic\" för automatisk identifiering av adressen.",
"Enter ignore patterns, one per line.": "Ange ignoreringsmönster, en per rad.",
"Enter up to three octal digits.": "Ange upp till tre oktala siffror.",
"Error": "Fel",
"Extended Attributes": "Utökade attribut",
"Extended Attributes Filter": "Utökat attributfilter",
"Extended Attributes Filter": "Filter för utökade attribut",
"External": "Extern",
"External File Versioning": "Extern filversionshantering",
"Failed Items": "Misslyckade objekt",
"Failed to load file versions.": "Det gick inte att läsa in filversioner.",
"Failed to load ignore patterns.": "Det gick inte att läsa in ignoreringsmönster.",
"Failed to set up, retrying": "Det gick inte att ställa in, försöker igen",
"Failure to connect to IPv6 servers is expected if there is no IPv6 connectivity.": "Det går inte att ansluta till IPv6-servrar om det inte finns någon IPv6-anslutning.",
"File Pull Order": "Filhämtningsprioritering",
"Failed to set up, retrying": "Det gick inte att konfigurera, försöker igen",
"Failure to connect to IPv6 servers is expected if there is no IPv6 connectivity.": "Det är normalt att anslutningar till IPv6-servrar misslyckas om IPv6-anslutning saknas.",
"File Pull Order": "Ordning för filhämtning",
"File Versioning": "Filversionshantering",
"Files are moved to .stversions directory when replaced or deleted by Syncthing.": "Filer flyttas till .stversions-mappen när de ersätts eller tas bort av Syncthing.",
"Files are moved to date stamped versions in a .stversions directory when replaced or deleted by Syncthing.": "Filer flyttas till datumstämplade versioner i en .stversions-mapp när de ersätts eller tas bort av Syncthing.",
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "Filer är skyddade från ändringar som görs på andra enheter, men ändringar som görs på den här enheten skickas till resten av klustret.",
"Files are synchronized from the cluster, but any changes made locally will not be sent to other devices.": "Filer synkroniseras från klustret, men alla ändringar som görs lokalt skickas inte till andra enheter.",
"Filesystem Watcher Errors": "Fel i filsystembevakaren",
"Files are synchronized from the cluster, but any changes made locally will not be sent to other devices.": "Filer synkroniseras från klustret, men inga lokala ändringar skickas till andra enheter.",
"Filesystem Watcher Errors": "Fel vid bevakning av filsystemet",
"Filter by date": "Filtrera efter datum",
"Filter by name": "Filtrera efter namn",
"Folder": "Mapp",
@@ -177,19 +177,19 @@
"Folder Path": "Mappsökväg",
"Folder Status": "Mappstatus",
"Folder Type": "Mapptyp",
"Folder type \"{%receiveEncrypted%}\" can only be set when adding a new folder.": "Mapptypen \"{{receiveEncrypted}}\" kan bara ställas in vid tilläggning av en ny mapp.",
"Folder type \"{%receiveEncrypted%}\" can only be set when adding a new folder.": "Mapptypen \"{{receiveEncrypted}}\" kan bara ställas in när en ny mapp läggs till.",
"Folder type \"{%receiveEncrypted%}\" cannot be changed after adding the folder. You need to remove the folder, delete or decrypt the data on disk, and add the folder again.": "Mapptypen \"{{receiveEncrypted}}\" kan inte ändras efter att mappen har lagts till. Du måste ta bort mappen, ta bort eller dekryptera data på disken och lägga till mappen igen.",
"Folders": "Mappar",
"For the following folders an error occurred while starting to watch for changes. It will be retried every minute, so the errors might go away soon. If they persist, try to fix the underlying issue and ask for help if you can't.": "För följande mappar uppstod ett fel när du började bevaka ändringar. Det kommer att omförsökas varje minut, så felen kan försvinna snart. Om de fortsätter, försök att åtgärda det underliggande problemet och fråga om hjälp om du inte kan.",
"For the following folders an error occurred while starting to watch for changes. It will be retried every minute, so the errors might go away soon. If they persist, try to fix the underlying issue and ask for help if you can't.": "För följande mappar uppstod ett fel när bevakning av ändringar skulle startas. Försöket upprepas varje minut, så felen kan snart försvinna. Om de fortsätter, försök att åtgärda det underliggande problemet och fråga om hjälp om du inte kan.",
"Forever": "För alltid",
"Full Rescan Interval (s)": "Intervall för fullständig omskanning (s)",
"GUI": "Grafiskt användargränssnitt",
"GUI / API HTTPS Certificate": "HTTPS-certifikat för gränssnitt / API",
"GUI / API HTTPS Certificate": "HTTPS-certifikat för gränssnitt och API",
"GUI Authentication Password": "Autentiseringslösenord för gränssnitt",
"GUI Authentication User": "Autentiseringsanvändare för gränssnitt",
"GUI Authentication: Set User and Password": "Autentisering för det grafiska gränssnittet: Ställ in användare och lösenord",
"GUI Authentication: Set User and Password": "Autentisering för det grafiska gränssnittet: ange användarnamn och lösenord",
"GUI Listen Address": "Lyssnaradress för gränssnitt",
"GUI Override Directory": "Åsidosätt mapp för gränssnitt",
"GUI Override Directory": "Mapp som åsidosätter gränssnittet",
"GUI Theme": "Tema för gränssnitt",
"General": "Allmänt",
"Generate": "Generera",
@@ -197,16 +197,16 @@
"Global Discovery Servers": "Globala annonseringsservrar",
"Global State": "Globalt tillstånd",
"Help": "Hjälp",
"Hint: only deny-rules detected while the default is deny. Consider adding \"permit any\" as last rule.": "Tips: endast neka-regler upptäcktes medan standard är neka. Överväg att lägga till \"tillåt alla\" som sista regel.",
"Hint: only deny-rules detected while the default is deny. Consider adding \"permit any\" as last rule.": "Tips: Endast nekande regler hittades samtidigt som standardåtgärden är att neka. Överväg att lägga till tillåt alla som sista regel.",
"Home page": "Webbplats",
"However, your current settings indicate you might not want it enabled. We have disabled automatic crash reporting for you.": "Dina aktuella inställningar indikerar dock att du kanske inte vill att ha det aktiverat. Vi har inaktiverat automatisk kraschrapportering för dig.",
"However, your current settings indicate you might not want it enabled. We have disabled automatic crash reporting for you.": "Dina aktuella inställningar tyder dock att du kanske inte vill ha det aktiverat. Vi har inaktiverat automatisk kraschrapportering för dig.",
"Identification": "Identifiering",
"If untrusted, enter encryption password": "Om opålitlig, ange krypteringslösenord",
"If you want to prevent other users on this computer from accessing Syncthing and through it your files, consider setting up authentication.": "Om du vill förhindra att andra användare på denna dator får åtkomst till Syncthing och dina filer via det, överväg att konfigurera autentisering.",
"If untrusted, enter encryption password": "Om enheten inte är betrodd anger du krypteringslösenordet",
"If you want to prevent other users on this computer from accessing Syncthing and through it your files, consider setting up authentication.": "Om du vill förhindra att andra användare på den här datorn får åtkomst till Syncthing och därigenom dina filer bör du konfigurera autentisering.",
"Ignore": "Ignorera",
"Ignore Patterns": "Ignoreringsmönster",
"Ignore Permissions": "Ignorera rättigheter",
"Ignore patterns can only be added after the folder is created. If checked, an input field to enter ignore patterns will be presented after saving.": "Ignoreringsmönster kan bara läggas till efter att mappen har skapats. Om markerad kommer ett inmatningsfält för att ange ignoreringsmönster att visas efter att du har sparat.",
"Ignore Permissions": "Ignorera behörigheter",
"Ignore patterns can only be added after the folder is created. If checked, an input field to enter ignore patterns will be presented after saving.": "Ignoreringsmönster kan endast läggas till efter att mappen har skapats. Om alternativet markeras visas ett inmatningsfält för ignoreringsmönster efter att du har sparat.",
"Ignored Devices": "Ignorerade enheter",
"Ignored Folders": "Ignorerade mappar",
"Ignored at": "Ignorerad vid",
@@ -214,12 +214,12 @@
"Incoming Rate Limit (KiB/s)": "Ingående hastighetsgräns (KiB/s)",
"Incorrect configuration may damage your folder contents and render Syncthing inoperable.": "Felaktig konfiguration kan skada innehållet i mappen och få Syncthing att sluta fungera.",
"Incorrect user name or password.": "Felaktigt användarnamn eller lösenord.",
"Info": "Info",
"Info": "Information",
"Internally used paths:": "Internt använda sökvägar:",
"Introduced By": "Introducerad av",
"Introducer": "Introduktör",
"Introduction": "Introduktion",
"Inversion of the given condition (i.e. do not exclude)": "Inversion av det givna tillståndet (d.v.s. exkluderar inte)",
"Inversion of the given condition (i.e. do not exclude)": "Omvändning av det angivna villkoret, det vill säga exkludera inte",
"Keep Versions": "Behåll versioner",
"LDAP": "LDAP",
"Largest First": "Största först",
@@ -236,9 +236,9 @@
"Listener Failures": "Lyssnarfel",
"Listener Status": "Lyssnarstatus",
"Listeners": "Lyssnare",
"Loading data...": "Läser in data...",
"Loading...": "Läser in...",
"Local Additions": "Lokalt tillägg",
"Loading data...": "Läser in data",
"Loading...": "Läser in",
"Local Additions": "Lokala tillägg",
"Local Discovery": "Lokal annonsering",
"Local State": "Lokalt tillstånd",
"Local State (Total)": "Lokalt tillstånd (totalt)",
@@ -249,11 +249,11 @@
"Log Out": "Logga ut",
"Log in to see paths information.": "Logga in för att se sökvägsinformation.",
"Log in to see version information.": "Logga in för att se versionsinformation.",
"Log tailing paused. Scroll to the bottom to continue.": "Loggning pausad. Bläddra till botten för att fortsätta.",
"Login failed, see Syncthing logs for details.": "Inloggningen misslyckades, se Syncthing-loggarna för detaljer.",
"Log tailing paused. Scroll to the bottom to continue.": "Den automatiska loggrullningen har pausats. Rulla längst ned för att fortsätta.",
"Login failed, see Syncthing logs for details.": "Inloggningen misslyckades. Mer information finns i Syncthings loggar.",
"Logs": "Loggar",
"Maintain an index of all blocks in the folder, enabling reuse of blocks from other files when syncing changes. Disable to reduce database size at the cost of not being able to reuse blocks across files.": "Upprätthåll ett index över alla block i mappen, vilket möjliggör återanvändning av block från andra filer vid synkronisering av ändringar. Inaktivera för att minska databasens storlek på bekostnad av att inte kunna återanvända block mellan filer.",
"Major Upgrade": "Större uppgradering",
"Major Upgrade": "Uppgradering av huvudversion",
"Mass actions": "Massåtgärder",
"Maximum Age": "Högsta ålder",
"Maximum single entry size": "Största tillåtna poststorlek",
@@ -262,19 +262,19 @@
"Minimum Free Disk Space": "Minsta lediga diskutrymme",
"Mod. Device": "Enhet som utförde ändring",
"Mod. Time": "Tid för ändring",
"More than a month ago": "Mer än en månad sedan",
"More than a month ago": "För mer än en månad sedan",
"More than a week ago": "För mer än en vecka sedan",
"More than a year ago": "Mer än ett år sedan",
"Move to top of queue": "Flytta till överst i kön",
"Multi level wildcard (matches multiple directory levels)": "Flernivå jokertecken (matchar flera mappnivåer)",
"Move to top of queue": "Flytta längst upp i kön",
"Multi level wildcard (matches multiple directory levels)": "Jokertecken på flera nivåer (matchar flera mappnivåer)",
"Never": "Aldrig",
"New Device": "Ny enhet",
"New Folder": "Ny mapp",
"Newest First": "Nyaste först",
"No": "Nej",
"No File Versioning": "Ingen filversionshantering",
"No files will be deleted as a result of this operation.": "Inga filer kommer att tas bort till följd av denna operation.",
"No rules set": "Inga regler fastställda",
"No files will be deleted as a result of this operation.": "Inga filer tas bort till följd av den här åtgärden.",
"No rules set": "Inga regler angivna",
"No upgrades": "Inga uppgraderingar",
"Not shared": "Inte delad",
"Notice": "Observera",
@@ -282,61 +282,61 @@
"OK": "OK",
"Off": "Av",
"Oldest First": "Äldsta först",
"Optional descriptive label for the folder. Can be different on each device.": "Valfri beskrivande etikett för mappen. Kan vara olika på varje enhet.",
"Optional group for the device. Can be different on each device.": "Valfri grupp för enheten. Kan vara olika på varje enhet.",
"Optional group for the folder. Can be different on each device.": "Valfri grupp för mappen. Kan vara olika på varje enhet.",
"Optional descriptive label for the folder. Can be different on each device.": "Valfri beskrivande etikett för mappen. Den kan skilja sig mellan enheterna.",
"Optional group for the device. Can be different on each device.": "Valfri grupp för enheten. Den kan skilja sig mellan enheterna.",
"Optional group for the folder. Can be different on each device.": "Valfri grupp för mappen. Den kan skilja sig mellan enheterna.",
"Options": "Alternativ",
"Out of Sync": "Osynkroniserad",
"Out of Sync Items": "Osynkroniserade objekt",
"Outgoing Rate Limit (KiB/s)": "Utgående hastighetsgräns (KiB/s)",
"Override": "Åsidosätt",
"Override Changes": "Åsidosätt förändringar",
"Override Changes": "Åsidosätt ändringar",
"Ownership": "Ägarskap",
"Password": "Lösenord",
"Path": "Sökväg",
"Path to the folder on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for": "Sökväg till mappen på din dator. Kommer att skapas om det inte finns. Tecknet tilde (~) kan användas som en genväg för",
"Path to the folder on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for": "Sökväg till mappen på din dator. Den skapas om den inte finns. Tecknet tilde (~) kan användas som en genväg för",
"Path where versions should be stored (leave empty for the default .stversions directory in the shared folder).": "Sökväg där versioner ska lagras (lämna tomt för standardmappen .stversions i den delade mappen).",
"Paths": "Sökvägar",
"Pause": "Pausa",
"Pause All": "Pausa alla",
"Paused": "Pausad",
"Paused (Unused)": "Pausad (oanvänd)",
"Pending changes": "Väntar på ändringar",
"Periodic scanning at given interval and disabled watching for changes": "Periodisk skanning vid givet intervall och inaktiverad bevakning av ändringar",
"Periodic scanning at given interval and enabled watching for changes": "Periodisk skanning vid givet intervall och aktiverad bevakning av ändringar",
"Periodic scanning at given interval and failed setting up watching for changes, retrying every 1m:": "Periodisk skanning vid givet intervall och misslyckades med att ställa in bevakning av ändringar, försöker igen var 1:e minut:",
"Permanently add it to the ignore list, suppressing further notifications.": "Lägg till det permanent i ignoreringslistan och undertryck ytterligare aviseringar.",
"Please consult the release notes before performing a major upgrade.": "Läs igenom versionsnyheterna innan du utför en större uppgradering.",
"Pending changes": "Väntande ändringar",
"Periodic scanning at given interval and disabled watching for changes": "Periodisk skanning körs med angivet intervall och bevakning av ändringar är inaktiverad",
"Periodic scanning at given interval and enabled watching for changes": "Periodisk skanning körs med angivet intervall och bevakning av ändringar är aktiverad",
"Periodic scanning at given interval and failed setting up watching for changes, retrying every 1m:": "Periodisk skanning körs med angivet intervall, men bevakning av ändringar kunde inte konfigureras. Ett nytt försök görs varje minut:",
"Permanently add it to the ignore list, suppressing further notifications.": "Lägg till den permanent i ignoreringslistan och dölj framtida aviseringar.",
"Please consult the release notes before performing a major upgrade.": "Läs versionsinformationen innan du uppgraderar till en ny huvudversion.",
"Please set a GUI Authentication User and Password in the Settings dialog.": "Ställ in en autentiseringsanvändare och ett lösenord för det grafiska användargränssnittet i inställningsdialogrutan.",
"Please wait": "Vänta",
"Prefix indicating that the file can be deleted if preventing directory removal": "Prefix som indikerar att filen kan tas bort om den förhindrar mappborttagning",
"Prefix indicating that the pattern should be matched without case sensitivity": "Prefix som indikerar att mönstret ska matchas utan skiftlägeskänslighet",
"Preparing to Sync": "Förberedelser för synkronisering",
"Preparing to Sync": "Förbereder synkronisering",
"Preview": "Förhandsgranska",
"Preview Usage Report": "Förhandsgranska användningsrapport",
"QR code": "QR-kod",
"QUIC LAN": "QUIC LAN",
"QUIC WAN": "QUIC WAN",
"Quick guide to supported patterns": "Snabb handledning till mönster som stöds",
"Quick guide to supported patterns": "Snabbguide till mönster som stöds",
"Random": "Slumpmässig",
"Receive Encrypted": "Ta emot krypterade",
"Receive Encrypted": "Ta emot krypterat",
"Receive Only": "Ta endast emot",
"Received data is already encrypted": "Mottagna data är redan krypterade",
"Recent Changes": "Senaste ändringar",
"Reduced by ignore patterns": "Minskas med ignoreringsmönster",
"Reduced by ignore patterns": "Minskad genom ignoreringsmönster",
"Relay LAN": "LAN-relä",
"Relay WAN": "WAN-relä",
"Release Notes": "Versionsanteckningar",
"Release Notes": "Versionsinformation",
"Release candidates contain the latest features and fixes. They are similar to the traditional bi-weekly Syncthing releases.": "Utgåvskandidater innehåller de senaste funktionerna och korrigeringarna. De liknar de traditionella Syncthing-utgåvorna som kommer ut varannan vecka.",
"Remote Devices": "Fjärrenheter",
"Remote GUI": "Fjärranslutet grafiskt användargränssnitt",
"Remote GUI": "Fjärrgränssnitt",
"Remove": "Ta bort",
"Remove Device": "Ta bort enhet",
"Remove Folder": "Ta bort mapp",
"Required identifier for the folder. Must be the same on all cluster devices.": "Krävs identifierare för mappen. Måste vara densamma på alla kluster enheter.",
"Required identifier for the folder. Must be the same on all cluster devices.": "Obligatorisk identifierare för mappen. Den måste vara identisk på alla enheter i klustret.",
"Rescan": "Skanna igen",
"Rescan All": "Skanna alla igen",
"Rescans": "Återkommande skanningar",
"Rescans": "Omskanningar",
"Restart": "Starta om",
"Restart Needed": "Omstart behövs",
"Restarting": "Startar om",
@@ -344,25 +344,25 @@
"Restore Versions": "Återställ versioner",
"Resume": "Återuppta",
"Resume All": "Återuppta alla",
"Reused": "Återanvänds",
"Revert": "Åter",
"Reused": "Återanvänt",
"Revert": "Återställ",
"Revert Local Changes": "Återställ lokala ändringar",
"Save": "Spara",
"Saving changes": "Sparar ändringar",
"Scan Time Remaining": "Återstående skanningstid",
"Scanning": "Skannar",
"See external versioning help for supported templated command line parameters.": "Se hjälp för extern version för stödda mallade kommandoradsparametrar.",
"See external versioning help for supported templated command line parameters.": "Se hjälpen för extern versionshantering för information om kommandoradsparametrar som stöder mallar.",
"Select All": "Markera alla",
"Select a version": "Välj en version",
"Select additional devices to share this folder with.": "Välj ytterligare enheter för att dela denna mapp med.",
"Select additional folders to share with this device.": "Välj ytterligare mappar att dela med denna enhet.",
"Select latest version": "Välj senaste versionen",
"Select oldest version": "Välj äldsta versionen",
"Send & Receive": "Skicka & ta emot",
"Send & Receive": "Skicka och ta emot",
"Send Extended Attributes": "Skicka utökade attribut",
"Send Only": "Skicka endast",
"Send Ownership": "Skicka ägarskap",
"Set Ignores on Added Folder": "Ställ in ignoreringar för tillagd mapp",
"Set Ignores on Added Folder": "Ange ignoreringsmönster för tillagd mapp",
"Settings": "Inställningar",
"Share": "Dela",
"Share Folder": "Dela mapp",
@@ -373,12 +373,12 @@
"Shared With": "Delas med",
"Sharing": "Delning",
"Show ID": "Visa ID",
"Show QR": "Visa QR",
"Show QR": "Visa QR-kod",
"Show detailed discovery status": "Visa detaljerad annonseringsstatus",
"Show detailed listener status": "Visa detaljerad lyssnarstatus",
"Show diff with previous version": "Visa skillnad med tidigare version",
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Visas istället för enhets-ID i klusterstatus. Kommer att annonseras andra enheter som ett valfritt standardnamn.",
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Visas istället för enhets-ID i klusterstatusen. Kommer att uppdateras till det namn som enheten annonserar om det lämnas tomt.",
"Show diff with previous version": "Visa skillnader mot föregående version",
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Visas i stället för enhets-ID i klusterstatusen. Namnet annonseras till andra enheter som ett valfritt standardnamn.",
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Visas i stället för enhets-ID i klusterstatusen. Om fältet lämnas tomt uppdateras det till det namn som enheten annonserar.",
"Shut Down": "Stäng av",
"Shutdown Complete": "Avstängning slutförd",
"Simple": "Enkel",
@@ -386,114 +386,114 @@
"Single level wildcard (matches within a directory only)": "Jokertecken på en nivå (matchar endast i en mapp)",
"Size": "Storlek",
"Smallest First": "Minsta först",
"Some discovery methods could not be established for finding other devices or announcing this device:": "Vissa annonseringsmetoder kunde inte fastställas för att hitta andra enheter eller tillkännage denna enhet:",
"Some discovery methods could not be established for finding other devices or announcing this device:": "Vissa annonseringsmetoder kunde inte upprättas för att hitta andra enheter eller annonsera denna enhet:",
"Some items could not be restored:": "Vissa objekt kunde inte återställas:",
"Some listening addresses could not be enabled to accept connections:": "Vissa lyssningsadresser kunde inte aktiveras för att acceptera anslutningar:",
"Source Code": "Källkod",
"Stable releases and release candidates": "Stabila utgåvor och utgåvskandidater",
"Stable releases are delayed by about two weeks. During this time they go through testing as release candidates.": "Stabila utgåvor är försenade med cirka två veckor. Under denna tid går de igenom testning som utgåvskandidater.",
"Stable releases are delayed by about two weeks. During this time they go through testing as release candidates.": "Stabila utgåvor publiceras cirka två veckor senare. Under tiden testas de som utgåvskandidater.",
"Stable releases only": "Endast stabila utgåvor",
"Staggered": "Förskjuten",
"Staggered File Versioning": "Filversionshantering i intervall",
"Start Browser": "Starta webbläsaren",
"Starting": "Börjar",
"Starting": "Startar",
"Statistics": "Statistik",
"Stay logged in": "Förbli inloggad",
"Stay logged in": "Håll mig inloggad",
"Stopped": "Stoppad",
"Stores and syncs only encrypted data. Folders on all connected devices need to be set up with the same password or be of type \"{%receiveEncrypted%}\" too.": "Lagrar och synkroniserar endast krypterade data. Mappar på alla anslutna enheter måste ställas in med samma lösenord eller också vara av typen \"{{receiveEncrypted}}\".",
"Subject:": "Ämne:",
"Support": "Support",
"Support Bundle": "Support Bundle",
"Support Bundle": "Supportpaket",
"Sync Extended Attributes": "Synkronisera utökade attribut",
"Sync Ownership": "Synkronisera ägarskap",
"Sync Protocol Listen Addresses": "Lyssnaradresser för synkroniseringsprotokoll",
"Sync Status": "Synkroniseringsstatus",
"Syncing": "Synkroniserar",
"Syncthing device ID for \"{%devicename%}\"": "Syncthing enhets-ID för \"{{devicename}}\"",
"Syncthing device ID for \"{%devicename%}\"": "Syncthings enhets-ID för \"{{devicename}}\"",
"Syncthing has been shut down.": "Syncthing har stängts av.",
"Syncthing includes the following software or portions thereof:": "Syncthing innehåller följande mjukvarupaket eller delar av dem:",
"Syncthing is Free and Open Source Software licensed as MPL v2.0.": "Syncthing har fri och öppen källkod licensierad som MPL v2.0.",
"Syncthing is a continuous file synchronization program. It synchronizes files between two or more computers in real time, safely protected from prying eyes. Your data is your data alone and you deserve to choose where it is stored, whether it is shared with some third party, and how it's transmitted over the internet.": "Syncthing är ett program för kontinuerlig filsynkronisering. Den synkroniserar filer mellan två eller flera datorer i realtid, säkert skyddade från nyfikna ögon. Dina data är dina data och du har rätt att välja var de lagras, om de delas med någon tredje part och hur de överförs över internet.",
"Syncthing includes the following software or portions thereof:": "Syncthing innehåller följande programvara eller delar av den:",
"Syncthing is Free and Open Source Software licensed as MPL v2.0.": "Syncthing är fri programvara med öppen källkod, licensierad enligt MPL v2.0.",
"Syncthing is a continuous file synchronization program. It synchronizes files between two or more computers in real time, safely protected from prying eyes. Your data is your data alone and you deserve to choose where it is stored, whether it is shared with some third party, and how it's transmitted over the internet.": "Syncthing är ett program för kontinuerlig filsynkronisering. Det synkroniserar filer mellan två eller flera datorer i realtid och skyddar dem från obehörig insyn. Dina data tillhör dig, och du ska kunna välja var de lagras, om de delas med en tredje part och hur de överförs via internet.",
"Syncthing is listening on the following network addresses for connection attempts from other devices:": "Syncthing lyssnar på följande nätverksadresser för anslutningsförsök från andra enheter:",
"Syncthing is not listening for connection attempts from other devices on any address. Only outgoing connections from this device may work.": "Syncthing lyssnar inte efter anslutningsförsök från andra enheter på någon adress. Endast utgående anslutningar från denna enhet kanske fungerar.",
"Syncthing is not listening for connection attempts from other devices on any address. Only outgoing connections from this device may work.": "Syncthing lyssnar inte efter anslutningsförsök från andra enheter på någon adress. Endast utgående anslutningar från den här enheten kan fungera.",
"Syncthing is restarting.": "Syncthing startar om.",
"Syncthing is saving changes.": "Syncthing sparar ändringar.",
"Syncthing is upgrading.": "Syncthing uppgraderas.",
"Syncthing now supports automatically reporting crashes to the developers. This feature is enabled by default.": "Syncthing stöder nu automatiskt kraschrapportering till utvecklarna. Denna funktion är aktiverad som standard.",
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing verkar vara avstängd, eller så finns det problem med din internetanslutning. Försöker igen…",
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Syncthing verkar ha drabbats av ett problem med behandlingen av din förfrågan. Uppdatera sidan eller starta om Syncthing om problemet kvarstår.",
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing verkar inte vara igång, eller så finns det problem med din internetanslutning. Försöker igen…",
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Syncthing verkar ha problem med att behandla din begäran. Uppdatera sidan eller starta om Syncthing om problemet kvarstår.",
"TCP LAN": "TCP LAN",
"TCP WAN": "TCP WAN",
"Take me back": "Ta mig tillbaka",
"The GUI address is overridden by startup options. Changes here will not take effect while the override is in place.": "Den grafiska gränssnittsadressen åsidosätts av startalternativ. Ändringar här träder inte i kraft så länge åsidosättningen är på plats.",
"The Syncthing Authors": "Syncthing-upphovsmän",
"The GUI address is overridden by startup options. Changes here will not take effect while the override is in place.": "Gränssnittets adress åsidosätts av startalternativ. Ändringar här får ingen effekt så länge åsidosättningen gäller.",
"The Syncthing Authors": "Syncthings upphovspersoner",
"The Syncthing admin interface is configured to allow remote access without a password.": "Administratörsgränssnittet för Syncthing är konfigurerat för att möjliggöra fjärråtkomst utan lösenord.",
"The aggregated statistics are publicly available at the URL below.": "Den aggregerade statistiken är offentligt tillgänglig på webbadressen nedan.",
"The cleanup interval cannot be blank.": "Rensningsintervallet kan inte vara tomt.",
"The aggregated statistics are publicly available at the URL below.": "Den sammanställda statistiken är offentligt tillgänglig på URL:en nedan.",
"The cleanup interval cannot be blank.": "Rensningsintervallet får inte lämnas tomt.",
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "Konfigurationen har sparats men inte aktiverats. Syncthing måste startas om för att aktivera den nya konfigurationen.",
"The device ID cannot be blank.": "Enhets-ID kan inte vara tomt.",
"The device ID cannot be blank.": "Enhets-ID får inte lämnas tomt.",
"The device ID to enter here can be found in the \"Actions > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "Enhets-ID som du anger här finns i dialogrutan \"Åtgärder > Visa ID\" på den andra enheten. Mellanslag och bindestreck är valfria (ignoreras).",
"The encrypted usage report is sent daily. It is used to track common platforms, folder sizes, and app versions. If the reported data set is changed you will be prompted with this dialog again.": "Den krypterade användningsrapporten skickas dagligen. Det används för att spåra vanliga plattformar, mappstorlekar och appversioner. Om den rapporterade datauppsättningen ändras kommer du att uppmanas med denna dialogruta igen.",
"The entered device ID does not look valid. It should be a 52 or 56 character string consisting of letters and numbers, with spaces and dashes being optional.": "Det inmatade enhets-ID:t verkar inte vara korrekt. Det ska vara en 52 eller 56 teckensträng bestående av siffror och bokstäver, eventuellt med mellanrum och bindestreck.",
"The encrypted usage report is sent daily. It is used to track common platforms, folder sizes, and app versions. If the reported data set is changed you will be prompted with this dialog again.": "Den krypterade användningsrapporten skickas dagligen. Den används för att kartlägga vanliga plattformar, mappstorlekar och programversioner. Om den rapporterade datauppsättningen ändras visas den här dialogrutan igen.",
"The entered device ID does not look valid. It should be a 52 or 56 character string consisting of letters and numbers, with spaces and dashes being optional.": "Det inmatade enhets-ID:t verkar inte vara korrekt. Det ska vara en sträng med 52 eller 56 tecken, bestående av bokstäver och siffror, där mellanslag och bindestreck är valfria.",
"The folder ID cannot be blank.": "Mapp-ID får inte vara tomt.",
"The folder ID must be unique.": "Mapp-ID måste vara unikt.",
"The folder content on other devices will be overwritten to become identical with this device. Files not present here will be deleted on other devices.": "Mappinnehållet på andra enheter skrivs över så att det blir identiskt med denna enhet. Filer som inte finns här kommer att tas bort andra enheter.",
"The folder content on this device will be overwritten to become identical with other devices. Files newly added here will be deleted.": "Mappinnehållet på denna enhet skrivs över för att bli identiskt med andra enheter. Filer som nyligen har lagts till här kommer att tas bort.",
"The folder content on other devices will be overwritten to become identical with this device. Files not present here will be deleted on other devices.": "Mappinnehållet på andra enheter skrivs över så att det blir identiskt med innehållet på den här enheten. Filer som inte finns här tas bort från de andra enheterna.",
"The folder content on this device will be overwritten to become identical with other devices. Files newly added here will be deleted.": "Mappinnehållet på den här enheten skrivs över att det blir identiskt med innehållet på de andra enheterna. Filer som nyligen har lagts till här tas bort.",
"The folder path cannot be blank.": "Mappsökvägen kan inte vara tom.",
"The following intervals are used: for the first hour a version is kept every 30 seconds, for the first day a version is kept every hour, for the first 30 days a version is kept every day, until the maximum age a version is kept every week.": "De följande intervallen används: varje 30 sekunder under den första timmen; varje timme under den första dagen; varje dag för de första 30 dagarna; varje vecka tills den maximala åldersgränsen uppnås.",
"The following intervals are used: for the first hour a version is kept every 30 seconds, for the first day a version is kept every hour, for the first 30 days a version is kept every day, until the maximum age a version is kept every week.": "Följande intervall används: under den första timmen sparas en version var 30:e sekund, under den första dagen en version varje timme, under de första 30 dagarna en version varje dag och därefter en version varje vecka tills den maximala åldern nås.",
"The following items could not be synchronized.": "Följande objekt kunde inte synkroniseras.",
"The following items were changed locally.": "Följande objekt ändrades lokalt.",
"The following methods are used to discover other devices on the network and announce this device to be found by others:": "Följande metoder används för att upptäcka andra enheter i nätverket och meddela att denna enhet ska hittas av andra:",
"The following methods are used to discover other devices on the network and announce this device to be found by others:": "Följande metoder används för att upptäcka andra enheter i nätverket och annonsera den här enheten så att andra kan hitta den:",
"The following text will automatically be inserted into a new message.": "Följande text kommer automatiskt att infogas i ett nytt meddelande.",
"The following unexpected items were found.": "Följande oväntade objekt hittades.",
"The interval must be a positive number of seconds.": "Intervallet måste vara ett positivt antal sekunder.",
"The interval, in seconds, for running cleanup in the versions directory. Zero to disable periodic cleaning.": "Intervallet, i sekunder, för att rensa i versionsmappen. Noll för att inaktivera periodisk rensning.",
"The maximum age must be a number and cannot be blank.": "Åldersgränsen måste vara ett tal och kan inte lämnas tomt.",
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "Den längsta tiden att behålla en version (i dagar, ställ in på 0 för att behålla versioner för alltid).",
"The interval, in seconds, for running cleanup in the versions directory. Zero to disable periodic cleaning.": "Intervallet i sekunder mellan rensningar av versionsmappen. Ange noll för att inaktivera periodisk rensning.",
"The maximum age must be a number and cannot be blank.": "Den högsta åldern måste vara ett tal och får inte lämnas tom.",
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "Den längsta tiden som en version ska behållas, i dagar. Ange 0 för att behålla versioner för alltid.",
"The number of connections must be a non-negative number.": "Antalet anslutningar måste vara ett icke-negativt tal.",
"The number of days must be a number and cannot be blank.": "Antalet dagar måste vara en siffra och får inte vara tomt.",
"The number of days must be a number and cannot be blank.": "Antalet dagar måste vara ett tal och får inte lämnas tomt.",
"The number of days to keep files in the trash can. Zero means forever.": "Antalet dagar som filer ligger kvar i papperskorgen. Noll betyder för alltid.",
"The number of old versions to keep, per file.": "Antalet gamla versioner som ska behållas, per fil.",
"The number of versions must be a number and cannot be blank.": "Antalet versioner måste vara ett nummer och kan inte lämnas tomt.",
"The number of old versions to keep, per file.": "Antalet gamla versioner som ska behållas per fil.",
"The number of versions must be a number and cannot be blank.": "Antalet versioner måste vara ett tal och får inte lämnas tomt.",
"The path cannot be blank.": "Sökvägen kan inte vara tom.",
"The rate limit is applied to the accumulated traffic of all connections to this device.": "Hastighetsgränsen tillämpas på den ackumulerade trafiken för alla anslutningar till denna enhet.",
"The rate limit is applied to the accumulated traffic of all connections to this device.": "Hastighetsgränsen tillämpas på den sammanlagda trafiken för alla anslutningar till den här enheten.",
"The rate limit must be a non-negative number (0: no limit)": "Hastighetsgränsen måste vara ett icke-negativt tal (0: ingen gräns)",
"The remote device has not accepted sharing this folder.": "Fjärrenheten har inte accepterat delning av denna mapp.",
"The remote device has not accepted sharing this folder.": "Fjärrenheten har inte godkänt att den här mappen delas.",
"The remote device has paused this folder.": "Fjärrenheten har pausat denna mapp.",
"The rescan interval must be a non-negative number of seconds.": "Omskanningsintervallet måste vara ett positivt antal sekunder.",
"The rescan interval must be a non-negative number of seconds.": "Omskanningsintervallet måste vara ett icke-negativt antal sekunder.",
"There are no devices to share this folder with.": "Det finns inga enheter att dela denna mapp med.",
"There are no file versions to restore.": "Det finns inga filversioner att återställa.",
"There are no folders to share with this device.": "Det finns inga mappar att dela med denna enhet.",
"They are retried automatically and will be synced when the error is resolved.": "De omprövas automatiskt och kommer att synkroniseras när felet är löst.",
"This Device": "Denna enhet",
"This Month": "Denna månad",
"This can easily give hackers access to read and change any files on your computer.": "Detta kan lätt ge hackare tillgång till att läsa och ändra några filer på datorn.",
"This device cannot automatically discover other devices or announce its own address to be found by others. Only devices with statically configured addresses can connect.": "Denna enhet kan inte automatiskt upptäcka andra enheter eller meddela sin egen adress som andra kan hitta. Endast enheter med statiskt konfigurerade adresser kan ansluta.",
"This is a major version upgrade.": "Det här är en stor uppgradering.",
"This setting controls the free space required on the home (i.e., index database) disk.": "Denna inställning styr hur mycket ledigt utrymme som krävs på hemdisken (dvs. indexdatabasen).",
"They are retried automatically and will be synced when the error is resolved.": "Försöken upprepas automatiskt och objekten synkroniseras när felet är löst.",
"This Device": "Den här enheten",
"This Month": "Den här månaden",
"This can easily give hackers access to read and change any files on your computer.": "Det kan enkelt ge hackare möjlighet att läsa och ändra alla filer på datorn.",
"This device cannot automatically discover other devices or announce its own address to be found by others. Only devices with statically configured addresses can connect.": "Denna enhet kan inte automatiskt upptäcka andra enheter eller annonsera sin egen adress så att andra kan hitta den. Endast enheter med statiskt konfigurerade adresser kan ansluta.",
"This is a major version upgrade.": "Det här är en uppgradering till en ny huvudversion.",
"This setting controls the free space required on the home (i.e., index database) disk.": "Den här inställningen styr hur mycket ledigt utrymme som krävs på disken där hemmappen, det vill säga indexdatabasen, finns.",
"Time": "Tid",
"Time the item was last modified": "Tidpunkten objektet var senast ändrad",
"Time the item was last modified": "Tidpunkt objektet senast ändrades",
"To connect with the Syncthing device named \"{%devicename%}\", add a new remote device on your end with this ID:": "För att ansluta till Syncthing-enheten med namnet \"{{devicename}}\", lägg till en ny fjärrenhet med detta ID:",
"To permit a rule, have the checkbox checked. To deny a rule, leave it unchecked.": "För att tillåta en regel, markera kryssrutan. För att neka en regel, lämna den omarkerad.",
"Today": "Idag",
"Trash Can": "Papperskorgen",
"Trash Can File Versioning": "Papperskorgs filversionshantering",
"Trash Can File Versioning": "Filversionshantering med papperskorg",
"Type": "Typ",
"UNIX Permissions": "UNIX-behörigheter",
"Unavailable": "Otillgänglig",
"Unavailable/Disabled by administrator or maintainer": "Otillgängligt/inaktiverat av administratör eller underhållare",
"Undecided (will prompt)": "Obeslutad (kommer att fråga)",
"Undecided (will prompt)": "Inte avgjort (fråga användaren)",
"Unexpected Items": "Oväntade objekt",
"Unexpected items have been found in this folder.": "Oväntade objekt har hittats i denna mapp.",
"Unignore": "Sluta ignorera",
"Unknown": "Okänd",
"Unshared": "Inte delad",
"Unshared Devices": "Odelade enheter",
"Unshared Folders": "Odelade mappar",
"Untrusted": "Opålitlig",
"Unshared Devices": "Enheter utan delade mappar",
"Unshared Folders": "Mappar som inte delas",
"Untrusted": "Inte betrodd",
"Up to Date": "Uppdaterad",
"Updated {%file%}": "Uppdaterade {{file}}",
"Upgrade": "Uppgradering",
"Upgrade": "Uppgradera",
"Upgrade To {%version%}": "Uppgradera till {{version}}",
"Upgrading": "Uppgraderar",
"Upload Rate": "Sändningshastighet",
@@ -502,8 +502,8 @@
"Use HTTPS for GUI": "Använd HTTPS för gränssnitt",
"Use notifications from the filesystem to detect changed items.": "Använd aviseringar från filsystemet för att upptäcka ändrade objekt.",
"User": "Användare",
"User Home": "Användarhem",
"Username/Password has not been set for the GUI authentication. Please consider setting it up.": "Användarnamn/lösenord har inte ställts in för autentisering av det grafiska gränssnittet. Överväg att ställa in det.",
"User Home": "Användarens hemmapp",
"Username/Password has not been set for the GUI authentication. Please consider setting it up.": "Användarnamn och lösenord har inte angetts för autentisering av det grafiska gränssnittet. Överväg att konfigurera dem.",
"Using a QUIC connection over LAN": "Använder en QUIC-anslutning över LAN",
"Using a QUIC connection over WAN": "Använder en QUIC-anslutning över WAN",
"Using a direct TCP connection over LAN": "Använder en direkt TCP-anslutning över LAN",
@@ -511,34 +511,34 @@
"Version": "Version",
"Versions": "Versioner",
"Versions Path": "Sökväg för versioner",
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Versioner tas automatiskt bort om de är äldre än den maximala åldern eller överskrider antalet filer som tillåts i ett intervall.",
"Waiting to Clean": "Väntar på att rensa",
"Waiting to Scan": "Väntar på att skanna",
"Waiting to Sync": "Väntar på att synkronisera",
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Versioner tas bort automatiskt om de är äldre än den högsta åldern eller om fler versioner lagras inom ett intervall än vad som tillåts.",
"Waiting to Clean": "Väntar på rensning",
"Waiting to Scan": "Väntar på skanning",
"Waiting to Sync": "Väntar på synkronisering",
"Warning": "Varning",
"Warning, this path is a parent directory of an existing folder \"{%otherFolder%}\".": "Varning, denna sökväg är en överordnad mapp för en befintlig mapp \"{{otherFolder}}\".",
"Warning, this path is a parent directory of an existing folder \"{%otherFolderLabel%}\" ({%otherFolder%}).": "Varning, denna sökväg är en överordnad mapp för en befintlig mapp \"{{otherFolderLabel}}\" ({{otherFolder}}).",
"Warning, this path is a subdirectory of an existing folder \"{%otherFolder%}\".": "Varning, denna sökväg är en undermapp för en befintlig mapp \"{{otherFolder}}\".",
"Warning, this path is a subdirectory of an existing folder \"{%otherFolderLabel%}\" ({%otherFolder%}).": "Varning, denna sökväg är en undermapp för en befintlig mapp \"{{otherFolderLabel}}\" ({{otherFolder}}).",
"Warning: If you are using an external watcher like {%syncthingInotify%}, you should make sure it is deactivated.": "Varning: Om du använder en extern bevakare som {{syncthingInotify}}, bör du se till att den är inaktiverad.",
"Watch for Changes": "Bevakning av ändringar",
"Watch for Changes": "Bevaka ändringar",
"Watching for Changes": "Bevakar ändringar",
"Watching for changes discovers most changes without periodic scanning.": "Bevakning av ändringar upptäcker de flesta ändringar utan periodisk skanning.",
"When adding a new device, keep in mind that this device must be added on the other side too.": "När du lägger till en ny enhet, kom ihåg att denna enhet måste läggas till på den andra enheten också.",
"When adding a new folder, keep in mind that the Folder ID is used to tie folders together between devices. They are case sensitive and must match exactly between all devices.": "När du lägger till ny mapp, tänk på att mapp-ID knyter ihop mappar mellan olika enheter. De är skiftlägeskänsliga och måste matcha precis mellan alla enheter.",
"When set to more than one on both devices, Syncthing will attempt to establish multiple concurrent connections. If the values differ, the highest will be used. Set to zero to let Syncthing decide.": "När den är inställd på mer än en på båda enheterna kommer Syncthing att försöka upprätta flera samtidiga anslutningar. Om värdena skiljer sig kommer det högsta att användas. Ställ in på noll för att låta Syncthing bestämma.",
"When adding a new device, keep in mind that this device must be added on the other side too.": "När du lägger till en ny enhet måste den här enheten även läggas till på den andra sidan.",
"When adding a new folder, keep in mind that the Folder ID is used to tie folders together between devices. They are case sensitive and must match exactly between all devices.": "När du lägger till en ny mapp bör du tänka på att mapp-ID:t används för att koppla ihop mappar mellan enheter. Mapp-ID:n är skiftlägeskänsliga och måste stämma exakt överens på alla enheter.",
"When set to more than one on both devices, Syncthing will attempt to establish multiple concurrent connections. If the values differ, the highest will be used. Set to zero to let Syncthing decide.": "När värdet är större än ett på båda enheterna försöker Syncthing upprätta flera samtidiga anslutningar. Om värdena skiljer sig används det högsta. Ange noll för att låta Syncthing bestämma.",
"Yes": "Ja",
"Yesterday": "Igår",
"You can also copy and paste the text into a new message manually.": "Du kan också kopiera och klistra in texten i ett nytt meddelande manuellt.",
"You can also select one of these nearby devices:": "Du kan också välja en av dessa närliggande enheter:",
"You can change your choice at any time in the Settings dialog.": "Du kan ändra ditt val när som helst i inställningsdialogrutan.",
"You can read more about the two release channels at the link below.": "Du kan läsa mer om de två publiceringsskanalerna länken nedan.",
"You can read more about the two release channels at the link below.": "Du kan läsa mer om de två utgivningskanalerna via länken nedan.",
"You have no ignored devices.": "Du har inga ignorerade enheter.",
"You have no ignored folders.": "Du har inga ignorerade mappar.",
"You have unsaved changes. Do you really want to discard them?": "Du har osparade ändringar. Vill du verkligen kassera dem?",
"You must keep at least one version.": "Du måste behålla åtminstone en version.",
"You should never add or change anything locally in a \"{%receiveEncrypted%}\" folder.": "Du ska aldrig lägga till eller ändra något lokalt i en \"{{receiveEncrypted}}\"-mapp.",
"Your SMS app should open to let you choose the recipient and send it from your own number.": "Din SMS-app bör öppnas så att du kan välja mottagare och skicka den från ditt eget nummer.",
"Your SMS app should open to let you choose the recipient and send it from your own number.": "Din sms-app bör öppnas så att du kan välja mottagare och skicka meddelandet från ditt eget nummer.",
"Your email app should open to let you choose the recipient and send it from your own address.": "Din e-postapp bör öppnas så att du kan välja mottagare och skicka den från din egen adress.",
"days": "dagar",
"deleted": "borttagen",
@@ -555,13 +555,13 @@
"theme": {
"name": {
"black": "Svart",
"dark": "Mörkt",
"dark": "Mörk",
"default": "Standard",
"light": "Ljust"
"light": "Ljus"
}
},
"unknown device": "okänd enhet",
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} vill dela mapp \"{{folder}}\".",
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} vill dela mapp \"{{folderlabel}}\" ({{folder}}).",
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} vill dela mappen \"{{folder}}\".",
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} vill dela mappen \"{{folderlabel}}\" ({{folder}}).",
"{%reintroducer%} might reintroduce this device.": "{{reintroducer}} kan återinföra denna enhet."
}
+10 -9
View File
@@ -222,7 +222,7 @@
<button type="button" class="btn btn-sm btn-danger" ng-click="ignoreDevice(deviceID, pendingDevice)" tooltip data-original-title="{{'Permanently add it to the ignore list, suppressing further notifications.' | translate}}">
<span class="fas fa-times"></span>&nbsp;<span translate>Ignore</span>
</button>
<button type="button" class="btn btn-sm btn-default" ng-click="dismissPendingDevice(deviceID)" tooltip data-original-title="{{'Do not add it to the ignore list, so this notification may recur.' | translate}}">
<button type="button" class="btn btn-sm btn-default" ng-click="dismissPendingDevice(deviceID)" tooltip data-original-title="{{'Do not add it to the ignore list, so this notification will reappear if the device connects again.' | translate}}">
<span class="far fa-clock"></span>&nbsp;<span translate>Dismiss</span>
</button>
</div>
@@ -269,7 +269,7 @@
<button type="button" class="btn btn-sm btn-danger" ng-click="ignoreFolder(deviceID, folderID, offeringDevice)" tooltip data-original-title="{{'Permanently add it to the ignore list, suppressing further notifications.' | translate}}">
<span class="fas fa-times"></span>&nbsp;<span translate>Ignore</span>
</button>
<button type="button" class="btn btn-sm btn-default" ng-click="dismissPendingFolder(folderID, deviceID)" tooltip data-original-title="{{'Do not add it to the ignore list, so this notification may recur.' | translate}}">
<button type="button" class="btn btn-sm btn-default" ng-click="dismissPendingFolder(folderID, deviceID)" tooltip data-original-title="{{'Do not add it to the ignore list, so this notification will reappear if the device offering this folder connects again.' | translate}}">
<span class="far fa-clock"></span>&nbsp;<span translate>Dismiss</span>
</button>
</div>
@@ -387,7 +387,7 @@
</h4>
<div class="panel-group" id="folders-{{ $index }}">
<div class="panel panel-default" ng-repeat="folder in groupedFolders">
<button class="btn panel-heading" data-toggle="collapse" data-parent="#folders-{{$index}}" data-target="#folder-{{$parent.$index}}-{{$index}}" aria-expanded="false">
<button class="btn panel-heading" data-toggle="collapse" data-parent="#folders-{{$parent.$index}}" data-target="#folder-{{$parent.$index}}-{{$index}}" aria-expanded="false">
<div class="panel-progress" ng-show="folderStatus(folder) == 'syncing'" ng-attr-style="width: {{syncPercentage(folder.id) | percent}}"></div>
<div class="panel-progress" ng-show="folderStatus(folder) == 'scanning' && scanProgress[folder.id] != undefined" ng-attr-style="width: {{scanPercentage(folder.id) | percent}}"></div>
<h4 class="panel-title">
@@ -410,8 +410,8 @@
</div>
</h4>
</button>
<div id="folder-{{$parent.$index}}-{{$index}}" class="panel-collapse collapse">
<div class="panel-body less-padding">
<div id="folder-{{$parent.$index}}-{{$index}}" class="panel-collapse collapse" lazy-collapse>
<div class="panel-body less-padding" ng-if="lazyReady">
<table class="table table-condensed table-striped table-auto">
<tbody>
<tr class="visible-xs">
@@ -686,8 +686,8 @@
<div class="panel-title-text">{{deviceName(deviceCfg)}}</div>
</h4>
</button>
<div id="device-this" class="panel-collapse collapse in">
<div class="panel-body less-padding">
<div id="device-this" class="panel-collapse collapse in" lazy-collapse>
<div class="panel-body less-padding" ng-if="lazyReady">
<table class="table table-condensed table-striped table-auto">
<tbody>
<tr>
@@ -805,8 +805,8 @@
<div class="panel-title-text">{{deviceName(deviceCfg)}}</div>
</h4>
</button>
<div id="device-{{$parent.$index}}-{{$index}}" class="panel-collapse collapse">
<div class="panel-body less-padding">
<div id="device-{{$parent.$index}}-{{$index}}" class="panel-collapse collapse" lazy-collapse>
<div class="panel-body less-padding" ng-if="lazyReady">
<table class="table table-condensed table-striped table-auto">
<tbody>
<tr class="visible-xs">
@@ -1083,6 +1083,7 @@
<script type="text/javascript" src="syncthing/core/popoverDirective.js"></script>
<script type="text/javascript" src="syncthing/core/syncthingController.js"></script>
<script type="text/javascript" src="syncthing/core/tooltipDirective.js"></script>
<script type="text/javascript" src="syncthing/core/lazyCollapseDirective.js"></script>
<script type="text/javascript" src="syncthing/core/uncamelFilter.js"></script>
<script type="text/javascript" src="syncthing/core/uniqueFolderDirective.js"></script>
<script type="text/javascript" src="syncthing/core/validDeviceidDirective.js"></script>
@@ -30,7 +30,7 @@
<h4 class="text-center" translate>The Syncthing Authors</h4>
<div class="row">
<div class="col-md-12" id="contributor-list">
Jakob Borg, Audrius Butkevicius, Simon Frei, Tomasz Wilczyński, Alexander Graf, Alexandre Viau, Anderson Mesquita, André Colomb, Antony Male, Ben Schulz, bt90, Caleb Callaway, Daniel Harte, Emil Lundberg, Eric P, Evgeny Kuznetsov, greatroar, Lars K.W. Gohlke, Lode Hoste, Marcus B Spencer, Michael Ploujnikov, Ross Smith II, Stefan Tatschner, Tommy van der Vorst, Wulf Weich, Adam Piggott, Adel Qalieh, Aleksey Vasenev, Alessandro G., Alex Ionescu, Alex Lindeman, Alex Xu, Alexander Seiler, Alexandre Alves, Aman Gupta, Andreas Sommer, andresvia, Andrew Rabert, Andrey D, andyleap, Anjan Momi, Anthony Goeckner, Antoine Lamielle, Anur, Aranjedeath, ardevd, Arkadiusz Tymiński, Aroun, Arthur Axel fREW Schmidt, Artur Zubilewicz, Ashish Bhate, Aurélien Rainone, BAHADIR YILMAZ, Bart De Vries, Beat Reichenbach, Ben Norcombe, Ben Shepherd, Ben Sidhom, Benedikt Heine, Benno Fünfstück, Benny Ng, boomsquared, Boqin Qin, Boris Rybalkin, Brendan Long, Catfriend1, Cathryne Linenweaver, Cedric Staniewski, Chih-Hsuan Yen, Choongkyu, Chris Howie, Chris Joel, Christian Kujau, Christian Prescott, chucic, cjc7373, Colin Kennedy, Cromefire_, cui, Cyprien Devillez, d-volution, Dan, Daniel Barczyk, Daniel Bergmann, Daniel Martí, Daniel Padrta, Daniil Gentili, Darshil Chanpura, dashangcun, David Rimmer, DeflateAwning, Denis A., Dennis Wilson, derekriemer, DerRockWolf, desbma, Devon G. Redekopp, digital, Dimitri Papadopoulos Orfanos, Dmitry Saveliev, domain, Domenic Horner, Dominik Heidler, Elias Jarlebring, Elliot Huffman, Emil Hessman, Eng Zer Jun, entity0xfe, Epifeny, epifeny, Eric Lesiuta, Erik Meitner, Evan Spensley, Federico Castagnini, Felix, Felix Ableitner, Felix Lampe, Felix Unterpaintner, Francois-Xavier Gsell, Frank Isemann, Gahl Saraf, georgespatton, ghjklw, Gilli Sigurdsson, Gleb Sinyavskiy, Graham Miln, Greg, guangwu, gudvinr, Gusted, Han Boetes, HansK-p, Harrison Jones, Hazem Krimi, Heiko Zuerker, Hireworks, Hugo Locurcio, Iain Barnett, Ian Johnson, ignacy123, Iskander Sharipov, Jaakko Hannikainen, Jack Croft, Jacob, Jake Peterson, James O'Beirne, James Patterson, Jaroslav Lichtblau, Jaroslav Malec, Jaspitta, Jaya Chithra, Jaya Kumar, Jeffery To, jelle van der Waa, Jens Diemer, Jochen Voss, Johan Vromans, John Rinehart, Jonas Thelemann, Jonathan, Jose Manuel Delicado, JRNitre, jtagcat, Julian Lehrhuber, Jörg Thalheim, Jędrzej Kula, Kapil Sareen, Karol Różycki, Kebin Liu, Keith Harrison, Kelong Cong, Ken'ichi Kamada, Kevin Allen, Kevin Bushiri, Kevin White, Jr., klemens, Kurt Fitzner, kylosus, Lars Lehtonen, Laurent Etiemble, Leo Arias, Liu Siyuan, Lord Landon Agahnim, LSmithx2, Luiz Angelo Daros de Luca, Lukas Lihotzki, Luke Hamburg, luzpaz, Majed Abdulaziz, Marc Laporte, Marcel Meyer, Marcin Dziadus, Marcus Legendre, Mario Majila, Mark Pulford, Martchus, Mateusz Naściszewski, Mateusz Ż, mathias4833, Matic Potočnik, Matt Burke, Matt Robenolt, Matteo Ruina, Maurizio Tomasi, Max, Max Schulze, MaximAL, Maximilian, Maxwell G, Michael Jephcote, Michael Rienstra, Michael Wang 汪東陽, MichaIng, Migelo, Mike Boone, MikeLund, MikolajTwarog, Mingxuan Lin, mv1005, Nate Morrison, nf, Nicholas Rishel, Nick Busey, Nico Stapelbroek, Nicolas Braud-Santoni, Nicolas Perraut, Niels Peter Roest, Nils Jakobi, NinoM4ster, Nitroretro, NoLooseEnds, Oliver Freyermuth, orangekame3, otbutz, overkill, Oyebanji Jacob Mayowa, Pablo, Pascal Jungblut, Paul Brit, Paul Donald, Pawel Palenica, perewa, Peter Badida, Peter Dave Hello, Peter Hoeg, Peter Marquardt, Phani Rithvij, Phil Davis, Philippe Schommers, Phill Luby, Piotr Bejda, polyfloyd, Prathik P Kulkarni, pullmerge, Quentin Hibon, Rahmi Pruitt, RealCharlesChia, red_led, Robert Carosi, Roberto Santalla, Robin Schoonover, Roman Zaynetdinov, rubenbe, Ruslan Yevdokymov, Ryan Qian, Ryan Sullivan, Sacheendra Talluri, Scott Klupfel, sec65, Sergey Mishin, Sertonix, Severin von Wnuck-Lipinski, Shaarad Dalvi, Shivam Kumar, Simon Mwepu, Simon Pickup, Sly_tom_cat, Sonu Kumar Saw, Stefan Kuntz, Steven Eckhoff, Suhas Gundimeda, Sven Bachmann, Sébastien WENSKE, Tao, Taylor Khan, Terrance, TheCreeper, Thomas, Thomas Hipp, Tim Abell, Tim Howes, Tobias Frölich, Tobias Klauser, Tobias Nygren, Tobias Tom, Tom Jakubowski, Tully Robinson, Tyler Brazier, Tyler Kropp, Umer-Azaz, Unrud, Val Markovic, vapatel2, Veeti Paananen, Victor Buinsky, Vik, Vil Brekin, villekalliomaki, Vladimir Rusinov, vvaswani, wangguoliang, WangXi, Will Rouesnel, William A. Kennington III, wouter bolsterlee, xarx00, Xavier O., xjtdy888, Yannic A., yparitcher, 佛跳墙, 落心
Jakob Borg, Audrius Butkevicius, Simon Frei, Tomasz Wilczyński, Alexander Graf, Alexandre Viau, Anderson Mesquita, André Colomb, Antony Male, Ben Schulz, bt90, Caleb Callaway, Daniel Harte, Emil Lundberg, Eric P, Evgeny Kuznetsov, greatroar, Lars K.W. Gohlke, Lode Hoste, Marcus B Spencer, Michael Ploujnikov, Ross Smith II, Stefan Tatschner, Tommy van der Vorst, Wulf Weich, Adam Piggott, Adel Qalieh, Aleksey Vasenev, Alessandro G., Alex Ionescu, Alex Lindeman, Alex Xu, Alexander Seiler, Alexandre Alves, Aman Gupta, Andreas Sommer, andresvia, Andrew Gunnerson, Andrew Rabert, Andrey D, andyleap, Anjan Momi, Anthony Goeckner, Antoine Lamielle, Anur, Aranjedeath, ardevd, Arkadiusz Tymiński, Aroun, Arthur Axel fREW Schmidt, Artur Zubilewicz, Ashish Bhate, Aurélien Rainone, BAHADIR YILMAZ, Bart De Vries, Beat Reichenbach, Ben Norcombe, Ben Shepherd, Ben Sidhom, Benedikt Heine, Benno Fünfstück, Benny Ng, boomsquared, Boqin Qin, Boris Rybalkin, Brendan Long, Catfriend1, Cathryne Linenweaver, Cedric Staniewski, Chih-Hsuan Yen, Choongkyu, Chris Howie, Chris Joel, Christian Kujau, Christian Prescott, chucic, cjc7373, Colin Kennedy, Cromefire_, cui, Cyprien Devillez, d-volution, Dan, Daniel Barczyk, Daniel Bergmann, Daniel Martí, Daniel Padrta, Daniil Gentili, Darshil Chanpura, dashangcun, David Rimmer, DeflateAwning, Denis A., Dennis Wilson, derekriemer, DerRockWolf, desbma, Devon G. Redekopp, digital, Dimitri Papadopoulos Orfanos, Dmitry Saveliev, domain, Domenic Horner, Dominik Heidler, Elias, Elias Jarlebring, Elliot Huffman, Emil Hessman, Eng Zer Jun, entity0xfe, Epifeny, epifeny, Eric Lesiuta, Erik Meitner, Evan Spensley, Federico Castagnini, Felix, Felix Ableitner, Felix Lampe, Felix Unterpaintner, Francois-Xavier Gsell, Frank Isemann, Frederic, Gahl Saraf, georgespatton, ghjklw, Gilli Sigurdsson, Gleb Sinyavskiy, Graham Miln, Greg, guangwu, gudvinr, Gusted, Han Boetes, HansK-p, Harrison Jones, Hazem Krimi, Heiko Zuerker, Henrik Bråthen, Hireworks, Hugo Locurcio, Iain Barnett, Ian Johnson, ignacy123, Iskander Sharipov, Jaakko Hannikainen, Jack Croft, Jacob, Jake Peterson, James O'Beirne, James Patterson, Jaroslav Lichtblau, Jaroslav Malec, Jaspitta, Jaya Chithra, Jaya Kumar, Jeffery To, jelle van der Waa, Jens Diemer, Jochen Voss, Johan Vromans, John Rinehart, Jonas Thelemann, Jonathan, Jose Manuel Delicado, JRNitre, jtagcat, Julian Lehrhuber, Jörg Thalheim, Jędrzej Kula, Kapil Sareen, Karol Różycki, Kebin Liu, Keith Harrison, Kelong Cong, Ken'ichi Kamada, Kevin Allen, Kevin Bushiri, Kevin White, Jr., klemens, Kurt Fitzner, kylosus, Lars Lehtonen, Laurent Etiemble, Leo Arias, Liu Siyuan, Lord Landon Agahnim, LSmithx2, Luiz Angelo Daros de Luca, Lukas Lihotzki, Luke Hamburg, luzpaz, Majed Abdulaziz, Marc Laporte, Marcel Meyer, Marcin Dziadus, Marcus Legendre, Mario Majila, Mark Pulford, Martchus, Mateusz Naściszewski, Mateusz Ż, mathias4833, Matic Potočnik, Matt Burke, Matt Robenolt, Matteo Ruina, mattn, Maurizio Tomasi, Max, Max Schulze, MaximAL, Maximilian, Maxwell G, Michael Jephcote, Michael Rienstra, Michael Wang 汪東陽, MichaIng, Migelo, Mike Boone, MikeLund, MikolajTwarog, Mingxuan Lin, mv1005, Nate Morrison, nf, Nicholas Rishel, Nick Busey, Nico Stapelbroek, Nicolas Braud-Santoni, Nicolas Perraut, Niels Peter Roest, Nils Jakobi, NinoM4ster, Nitroretro, NoLooseEnds, Oliver Freyermuth, orangekame3, otbutz, overkill, Oyebanji Jacob Mayowa, Pablo, Pascal Jungblut, Paul Brit, Paul Donald, Pawel Palenica, perewa, Peter Badida, Peter Dave Hello, Peter Hoeg, Peter Marquardt, Phani Rithvij, Phil Davis, Philippe Schommers, Phill Luby, Piotr Bejda, polyfloyd, Prathik P Kulkarni, pullmerge, Quentin Hibon, Rahmi Pruitt, RealCharlesChia, red_led, Robert Carosi, Roberto Santalla, Robin Schoonover, Rohit Tanwar, Roman Zaynetdinov, rubenbe, Ruslan Yevdokymov, Ryan Qian, Ryan Sullivan, Sacheendra Talluri, Scott Klupfel, sec65, Sergey Mishin, Sertonix, Severin von Wnuck-Lipinski, Shaarad Dalvi, Shablone, Shivam Kumar, Simon Mwepu, Simon Pickup, Sly_tom_cat, Sonu Kumar Saw, Stefan Kuntz, Steven Eckhoff, Suhas Gundimeda, Sven Bachmann, Sébastien WENSKE, Tao, Taylor Khan, tbodt, Terrance, TheCreeper, Thomas, Thomas Hipp, Tim Abell, Tim Howes, Tobias Frölich, Tobias Klauser, Tobias Nygren, Tobias Tom, Tom Jakubowski, Tully Robinson, Tyler Brazier, Tyler Kropp, Umer-Azaz, Unrud, Val Markovic, vapatel2, Veeti Paananen, Victor Buinsky, Vik, Vil Brekin, villekalliomaki, Vladimir Rusinov, vvaswani, wangguoliang, WangXi, Will Rouesnel, William A. Kennington III, wouter bolsterlee, xarx00, Xavier O., xjtdy888, Yannic A., yparitcher, 佛跳墙, 落心
</div>
</div>
</div>
@@ -62,7 +62,6 @@ Jakob Borg, Audrius Butkevicius, Simon Frei, Tomasz Wilczyński, Alexander Graf,
<li><a href="https://github.com/davecgh/go-spew">davecgh/go-spew</a>, Copyright &copy; 2012-2016 Dave Collins.</li>
<li><a href="https://github.com/go-asn1-ber/asn1-ber">go-asn1-ber/asn1-ber</a>, Copyright &copy; 2011-2015 Michael Mitton (mmitton@gmail.com).</li>
<li><a href="https://github.com/go-ldap/ldap">go-ldap/ldap</a>, Copyright &copy; 2011-2015 Michael Mitton (mmitton@gmail.com).</li>
<li><a href="https://go.yaml.in/yaml/v2">go.yaml.in/yaml/v2</a>, Copyright &copy; 2026, the go.yaml.in/yaml/v2 authors.</li>
<li><a href="https://github.com/gobwas/glob">gobwas/glob</a>, Copyright &copy; 2016 Sergey Kamardin.</li>
<li><a href="https://github.com/gofrs/flock">gofrs/flock</a>, Copyright &copy; 2018-2025, The Gofrs.</li>
<li><a href="https://github.com/golang/snappy">golang/snappy</a>, Copyright &copy; 2011 The Snappy-Go Authors.</li>
@@ -0,0 +1,25 @@
angular.module('syncthing.core')
.directive('lazyCollapse', function () {
return {
restrict: 'A',
link: function (scope, element) {
// Render panel content only while expanded. Collapsed panels
// contribute zero watchers. Bootstrap's show/hidden events
// fire before/after the animation, so the DOM is present
// during the transition.
scope.lazyReady = element.hasClass('in');
$(element).on('show.bs.collapse', function () {
scope.$apply(function () {
scope.lazyReady = true;
});
});
$(element).on('hidden.bs.collapse', function () {
scope.$apply(function () {
scope.lazyReady = false;
});
});
}
};
});
@@ -1139,20 +1139,20 @@ angular.module('syncthing.core')
};
$scope.syncPercentage = function (folder) {
if (typeof $scope.model[folder] === 'undefined') {
var model = $scope.model[folder];
if (typeof model === 'undefined') {
return 100;
}
if ($scope.model[folder].needTotalItems === 0) {
if (model.needTotalItems === 0) {
return 100;
}
if (($scope.model[folder].needBytes == 0 && $scope.model[folder].needDeletes > 0) || $scope.model[folder].globalBytes == 0) {
// We don't need any data, but we have deletes that we need
// to do. Drop down the completion percentage to indicate
// that we have stuff to do.
// Do the same thing in case we only have zero byte files to sync.
if (model.needBytes == 0 && model.needTotalItems > 0) {
// We don't need any data, but we have deletes, directories,
// symlinks or zero byte files that we need to do. Drop down the
// completion percentage to indicate that we have stuff to do.
return 95;
}
return progressIntegerPercentage($scope.model[folder].inSyncBytes, $scope.model[folder].globalBytes);
return progressIntegerPercentage(model.inSyncBytes, model.globalBytes);
};
$scope.scanPercentage = function (folder) {
@@ -2997,6 +2997,9 @@ angular.module('syncthing.core')
$scope.restoreVersions.tree = $("#restoreTree").fancytree({
extensions: ["table", "filter", "glyph"],
quicksearch: true,
// Node titles are remote-controlled file/path
// components; render them as text, not HTML.
escapeTitles: true,
filter: {
hideExpanders: true,
mode: "hide"
+1
View File
@@ -79,6 +79,7 @@ type DB interface {
// Cleanup
DropAllFiles(folder string, device protocol.DeviceID) error
DropFolderDevice(folder string, device protocol.DeviceID) error
DropDevice(device protocol.DeviceID) error
DropFilesNamed(folder string, device protocol.DeviceID, names []string) error
DropFolder(folder string) error
+5
View File
@@ -128,6 +128,11 @@ func (m metricsDB) DropAllFiles(folder string, device protocol.DeviceID) error {
return m.DB.DropAllFiles(folder, device)
}
func (m metricsDB) DropFolderDevice(folder string, device protocol.DeviceID) error {
defer m.account(folder, "DropFolderDevice")()
return m.DB.DropFolderDevice(folder, device)
}
func (m metricsDB) DropDevice(device protocol.DeviceID) error {
defer m.account("-", "DropDevice")()
return m.DB.DropDevice(device)
+1 -1
View File
@@ -27,7 +27,7 @@ import (
)
const (
currentSchemaVersion = 5
currentSchemaVersion = 6
applicationIDMain = 0x53546d6e // "STmn", Syncthing main database
applicationIDFolder = 0x53546664 // "STfd", Syncthing folder database
)
+11
View File
@@ -255,6 +255,17 @@ func (s *DB) DropAllFiles(folder string, device protocol.DeviceID) error {
return fdb.DropAllFiles(device)
}
func (s *DB) DropFolderDevice(folder string, device protocol.DeviceID) error {
fdb, err := s.getFolderDB(folder, false)
if errors.Is(err, errNoSuchFolder) {
return nil
}
if err != nil {
return err
}
return fdb.DropDevice(device)
}
func (s *DB) DropFilesNamed(folder string, device protocol.DeviceID, names []string) error {
fdb, err := s.getFolderDB(folder, false)
if errors.Is(err, errNoSuchFolder) {
+86 -6
View File
@@ -13,7 +13,6 @@ import (
"errors"
"iter"
"os"
"path"
"path/filepath"
"sync"
"testing"
@@ -30,7 +29,6 @@ import (
const (
folderID = "test"
blockSize = 128 << 10
dirSize = 128
)
func TestBasics(t *testing.T) {
@@ -73,11 +71,11 @@ func TestBasics(t *testing.T) {
t.Fatal(err)
}
const (
localSize = (1+2+3)*blockSize + dirSize
localSize = (1+2+3)*blockSize
remoteSize = (3 + 4 + 5) * blockSize
globalSize = (2+3+3+4+5)*blockSize + dirSize
globalSize = (2+3+3+4+5)*blockSize
needSizeLocal = remoteSize
needSizeRemote = (2+3)*blockSize + dirSize
needSizeRemote = (2+3)*blockSize
)
t.Run("SchemaVersion", func(t *testing.T) {
@@ -799,6 +797,13 @@ func TestDropAllFiles(t *testing.T) {
t.Fatal(err)
}
// The sequence is non-zero before the drop
if seq, err := db.GetDeviceSequence("a", protocol.DeviceID{1}); err != nil {
t.Fatal(err)
} else if seq == 0 {
t.Error("expected non-zero sequence before drop")
}
// Drop folder A
if err := db.DropAllFiles("a", protocol.DeviceID{1}); err != nil {
t.Fatal(err)
@@ -826,6 +831,20 @@ func TestDropAllFiles(t *testing.T) {
t.Error("expected count to be two")
}
// The device sequence for the dropped folder is reset to zero.
if seq, err := db.GetDeviceSequence("a", protocol.DeviceID{1}); err != nil {
t.Fatal(err)
} else if seq != 0 {
t.Log(seq)
t.Error("expected sequence to be reset to zero after DropAllFiles")
}
// Sequence for the untouched folder is unaffected.
if seq, err := db.GetDeviceSequence("b", protocol.DeviceID{1}); err != nil {
t.Fatal(err)
} else if seq == 0 {
t.Error("expected non-zero sequence for untouched folder")
}
// Drop things that don't exist
if err := db.DropAllFiles("a", protocol.DeviceID{99}); err != nil {
t.Fatal(err)
@@ -838,6 +857,67 @@ func TestDropAllFiles(t *testing.T) {
}
}
func TestDropFolderDevice(t *testing.T) {
db, err := Open(t.TempDir())
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
if err := db.Close(); err != nil {
t.Fatal(err)
}
})
// Files from device 1 in folder a
err = db.Update("a", protocol.DeviceID{1}, []protocol.FileInfo{
genFile("test1", 1, 101),
genFile("test2", 2, 102),
})
if err != nil {
t.Fatal(err)
}
// Device 1 has an index ID.
if err := db.SetIndexID("a", protocol.DeviceID{1}, protocol.IndexID(0xdeadbeef)); err != nil {
t.Fatal(err)
}
if id, err := db.GetIndexID("a", protocol.DeviceID{1}); err != nil {
t.Fatal(err)
} else if id != protocol.IndexID(0xdeadbeef) {
t.Errorf("expected index ID to be set, got %v", id)
}
// Drop device 1 from folder a
if err := db.DropFolderDevice("a", protocol.DeviceID{1}); err != nil {
t.Fatal(err)
}
// Files for device 1 in folder a are gone
if _, ok, err := db.GetDeviceFile("a", protocol.DeviceID{1}, "test1"); err != nil || ok {
t.Log(err, ok)
t.Error("expected device 1 file in folder A to not exist")
}
if c, err := db.CountLocal("a", protocol.DeviceID{1}); err != nil {
t.Fatal(err)
} else if c.Files != 0 {
t.Log(c)
t.Error("expected device 1 count in folder A to be zero")
}
// The index ID for device 1 in folder A is gone.
if id, err := db.GetIndexID("a", protocol.DeviceID{1}); err != nil {
t.Fatal(err)
} else if id != 0 {
t.Errorf("expected index ID to be cleared, got %v", id)
}
if seq, err := db.GetDeviceSequence("a", protocol.DeviceID{1}); err != nil {
t.Fatal(err)
} else if seq != 0 {
t.Log(seq)
t.Error("expected sequence to be zero after DropFolderDevice")
}
}
func TestConcurrentUpdate(t *testing.T) {
t.Parallel()
@@ -1168,7 +1248,7 @@ func TestOpenSpecialName(t *testing.T) {
// Create a "base" dir that is in the way if the path becomes
// incorrectly truncated in the next steps.
base := path.Join(dir, "test")
base := filepath.Join(dir, "test")
if err := os.Mkdir(base, 0o755); err != nil {
t.Fatal(err)
}
+7 -4
View File
@@ -111,10 +111,6 @@ func (s *folderDB) Update(device protocol.DeviceID, fs []protocol.FileInfo, opti
f.BlocksHash = nil
}
if f.Type == protocol.FileInfoTypeDirectory {
f.Size = 128 // synthetic directory size
}
// Insert the file.
//
// If it is a remote file, set remote_sequence otherwise leave it at
@@ -235,6 +231,13 @@ func (s *folderDB) DropAllFiles(device protocol.DeviceID) error {
defer tx.Rollback() //nolint:errcheck
txp := &txPreparedStmts{Tx: tx}
if _, err := tx.Exec(`
UPDATE indexids SET sequence = 0
WHERE device_idx = ?
`, deviceIdx); err != nil {
return wrap(err)
}
// Drop all the file entries
result, err := tx.Exec(`
@@ -0,0 +1,16 @@
-- Copyright (C) 2026 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/.
-- All non-file entries should have size zero.
-- Directories were previously stored with a "synthetic" size of 128.
UPDATE files
SET size = 0
WHERE type != 0
;
UPDATE counts
SET size = 0
WHERE type != 0
;
+4
View File
@@ -106,6 +106,10 @@ func (i indirectFI) FileInfo() (protocol.FileInfo, error) {
fi.Blocks = bl.Blocks
}
fi.Name = osutil.NativeFilename(fi.Name)
// Undo earlier behaviour of setting a synthetic size on dirs.
if fi.Type != protocol.FileInfoTypeFile && fi.Size > 0 {
fi.Size = 0
}
return protocol.FileInfoFromDB(&fi), nil
}
+2 -2
View File
@@ -1248,7 +1248,7 @@ func (s *service) getSupportBundle(w http.ResponseWriter, r *http.Request) {
zipFilePath := filepath.Join(locations.GetBaseDir(locations.ConfigBaseDir), zipFileName)
// Write buffer zip to local zip file (back up)
if err := os.WriteFile(zipFilePath, zipFilesBuffer.Bytes(), 0o600); err != nil {
if err := os.WriteFile(zipFilePath, zipFilesBuffer.Bytes(), 0o666); err != nil {
slog.Warn("Failed to create support bundle zip (file)", slogutil.FilePath(zipFilePath), slogutil.Error(err))
}
@@ -1774,7 +1774,7 @@ func fileIntfJSONMap(f protocol.FileInfo) map[string]interface{} {
out := map[string]interface{}{
"name": f.FileName(),
"type": f.FileType().String(),
"size": f.FileSize(),
"size": f.Size,
"deleted": f.IsDeleted(),
"invalid": f.IsInvalid(),
"ignored": f.IsIgnored(),
+1 -5
View File
@@ -74,11 +74,7 @@ func (s *staticsServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
func (s *staticsServer) serveAsset(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Cache-Control", "no-cache, must-revalidate")
file := r.URL.Path
if file[0] == '/' {
file = file[1:]
}
file := strings.TrimPrefix(r.URL.Path, "/")
if file == "" {
file = "index.html"
+9 -3
View File
@@ -1762,9 +1762,10 @@ func TestConfigChanges(t *testing.T) {
folder2Path := "/rest/config/folders/folder2"
// Create a folder and add another
// Create a folder and add another. Give folder2 a non-default
// RescanIntervalS so we can verify it survives a later partial PATCH.
mod(http.MethodPut, "/rest/config/folders", []config.FolderConfiguration{{ID: "folder1", Path: "folder1"}})
mod(http.MethodPut, folder2Path, config.FolderConfiguration{ID: "folder2", Path: "folder2"})
mod(http.MethodPut, folder2Path, config.FolderConfiguration{ID: "folder2", Path: "folder2", RescanIntervalS: 1234})
// Check they are there
get("/rest/config/folders/folder1").Body.Close()
@@ -1779,9 +1780,14 @@ func TestConfigChanges(t *testing.T) {
if err := unmarshalTo(resp.Body, &folder); err != nil {
t.Fatal(err)
}
if !dev.Paused {
if !folder.Paused {
t.Error("Expected folder to be paused")
}
// A partial PATCH must not reset other (default-tagged) attributes to
// their default values.
if folder.RescanIntervalS != 1234 {
t.Error("Expected RescanIntervalS to be preserved as 1234, got", folder.RescanIntervalS)
}
// Delete folder2
req, _ := http.NewRequest(http.MethodDelete, baseURL+folder2Path, nil)
-1
View File
@@ -116,7 +116,6 @@
<autoUpgradeIntervalH>12</autoUpgradeIntervalH>
<upgradeToPreReleases>false</upgradeToPreReleases>
<keepTemporariesH>24</keepTemporariesH>
<cacheIgnoredFiles>false</cacheIgnoredFiles>
<progressUpdateIntervalS>5</progressUpdateIntervalS>
<limitBandwidthInLan>false</limitBandwidthInLan>
<minHomeDiskFree unit="%">1</minHomeDiskFree>
+1 -1
View File
@@ -16,7 +16,7 @@ import (
// or, somewhere along the way the "+" in the version tag disappeared:
// syncthing v1.23.7-dev.26.gdf7b56ae.dirty-stversionextra "Fermium Flea" (go1.20.5 darwin-arm64) jb@ok.kastelo.net 2023-07-12 06:55:26 UTC [Some Wrapper, purego, stnoupgrade]
var (
longVersionRE = regexp.MustCompile(`syncthing\s+(v[^\s]+)\s+"([^"]+)"\s\(([^\s]+)\s+([^-]+)-([^)]+)\)\s+([^\s]+)[^\[]*(?:\[(.+)\])?$`)
longVersionRE = regexp.MustCompile(`syncthing\s+(v[^\s]+)\s+"([^"]+)"\s\(([^\s]+)\s+([^-]+)-([^)]+)\)\s+([^\s]+)[^\[]*(?:\[(.+)\])?`)
gitExtraRE = regexp.MustCompile(`\.\d+\.g[0-9a-f]+`) // ".1.g6aaae618"
gitExtraSepRE = regexp.MustCompile(`[.-]`) // dot or dash
)
+14
View File
@@ -57,6 +57,20 @@ func TestParseVersion(t *testing.T) {
Extra: []string{"Some Wrapper", "purego", "stnoupgrade"},
},
},
{
longVersion: `2026-05-18 14:53:32 INF syncthing v2.0.3 "Hafnium Hornet" (go1.25.0 darwin-amd64) builder@github.syncthing.net 2025-08-22 07:00:05 UTC [stnoupgrade] (log.pkg=main)`,
parsed: VersionParts{
Version: "v2.0.3",
Tag: "v2.0.3",
Commit: "",
Codename: "Hafnium Hornet",
Runtime: "go1.25.0",
GOOS: "darwin",
GOARCH: "amd64",
Builder: "builder@github.syncthing.net",
Extra: []string{"stnoupgrade"},
},
},
}
for _, tc := range cases {
-2
View File
@@ -72,7 +72,6 @@ func TestDefaultValues(t *testing.T) {
NATTimeoutS: 10,
AutoUpgradeIntervalH: 12,
KeepTemporariesH: 24,
CacheIgnoredFiles: false,
ProgressUpdateIntervalS: 5,
LimitBandwidthInLan: false,
MinHomeDiskFree: Size{1, "%"},
@@ -278,7 +277,6 @@ func TestOverriddenValues(t *testing.T) {
NATTimeoutS: 15,
AutoUpgradeIntervalH: 24,
KeepTemporariesH: 48,
CacheIgnoredFiles: true,
ProgressUpdateIntervalS: 10,
LimitBandwidthInLan: true,
MinHomeDiskFree: Size{5.2, "%"},
+3 -23
View File
@@ -9,7 +9,6 @@ package config
import (
"bytes"
"crypto/sha256"
"encoding/json"
"encoding/xml"
"errors"
"fmt"
@@ -168,7 +167,7 @@ func (f *FolderConfiguration) CreateMarker() error {
ffs := f.Filesystem()
// Create the marker as a directory
err := ffs.Mkdir(DefaultMarkerName, 0o755)
err := ffs.Mkdir(DefaultMarkerName, fs.ModePerm)
if err != nil {
return err
}
@@ -176,7 +175,7 @@ func (f *FolderConfiguration) CreateMarker() error {
// Create a file inside it, reducing the risk of the marker directory
// being removed by automated cleanup tools.
markerFile := filepath.Join(DefaultMarkerName, f.markerFilename())
if err := fs.WriteFile(ffs, markerFile, f.markerContents(), 0o644); err != nil {
if err := fs.WriteFile(ffs, markerFile, f.markerContents(), 0o666); err != nil {
return err
}
@@ -246,19 +245,10 @@ func (f *FolderConfiguration) checkFilesystemPath(ffs fs.Filesystem, path string
}
func (f *FolderConfiguration) CreateRoot() (err error) {
// Directory permission bits. Will be filtered down to something
// sane by umask on Unixes.
permBits := fs.FileMode(0o777)
if build.IsWindows {
// Windows has no umask so we must chose a safer set of bits to
// begin with.
permBits = 0o700
}
filesystem := f.Filesystem()
if _, err = filesystem.Stat("."); fs.IsNotExist(err) {
err = filesystem.MkdirAll(".", permBits)
err = filesystem.MkdirAll(".", fs.ModePerm)
}
return err
@@ -404,16 +394,6 @@ func (f XattrFilter) GetMaxTotalSize() int {
return f.MaxTotalSize
}
func (f *FolderConfiguration) UnmarshalJSON(data []byte) error {
structutil.SetDefaults(f)
// avoid recursing into this method
type noCustomUnmarshal FolderConfiguration
ptr := (*noCustomUnmarshal)(f)
return json.Unmarshal(data, ptr)
}
func (f *FolderConfiguration) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
structutil.SetDefaults(f)
+6 -18
View File
@@ -18,7 +18,6 @@ import (
"sync"
"github.com/syncthing/syncthing/internal/slogutil"
"github.com/syncthing/syncthing/lib/build"
"github.com/syncthing/syncthing/lib/fs"
"github.com/syncthing/syncthing/lib/netutil"
"github.com/syncthing/syncthing/lib/upgrade"
@@ -224,27 +223,20 @@ func migrateToConfigV24(cfg *Configuration) {
}
func migrateToConfigV23(cfg *Configuration) {
permBits := fs.FileMode(0o777)
if build.IsWindows {
// Windows has no umask so we must chose a safer set of bits to
// begin with.
permBits = 0o700
}
// Upgrade code remains hardcoded for .stfolder despite configurable
// marker name in later versions.
for i := range cfg.Folders {
fs := cfg.Folders[i].Filesystem()
ffs := cfg.Folders[i].Filesystem()
// Invalid config posted, or tests.
if fs == nil {
if ffs == nil {
continue
}
if stat, err := fs.Stat(DefaultMarkerName); err == nil && !stat.IsDir() {
err = fs.Remove(DefaultMarkerName)
if stat, err := ffs.Stat(DefaultMarkerName); err == nil && !stat.IsDir() {
err = ffs.Remove(DefaultMarkerName)
if err == nil {
err = fs.Mkdir(DefaultMarkerName, permBits)
fs.Hide(DefaultMarkerName) // ignore error
err = ffs.Mkdir(DefaultMarkerName, fs.ModePerm)
ffs.Hide(DefaultMarkerName) // ignore error
}
if err != nil {
slog.Warn("Failed to upgrade folder marker", slogutil.Error(err))
@@ -328,10 +320,6 @@ func migrateToConfigV15(cfg *Configuration) {
}
func migrateToConfigV14(cfg *Configuration) {
// Not using the ignore cache is the new default. Disable it on existing
// configurations.
cfg.Options.CacheIgnoredFiles = false
// Migrate UPnP -> NAT options
cfg.Options.NATEnabled = cfg.Options.DeprecatedUPnPEnabled
cfg.Options.DeprecatedUPnPEnabled = false
-1
View File
@@ -45,7 +45,6 @@ type OptionsConfiguration struct {
AutoUpgradeIntervalH int `json:"autoUpgradeIntervalH" xml:"autoUpgradeIntervalH" default:"12"`
UpgradeToPreReleases bool `json:"upgradeToPreReleases" xml:"upgradeToPreReleases"`
KeepTemporariesH int `json:"keepTemporariesH" xml:"keepTemporariesH" default:"24"`
CacheIgnoredFiles bool `json:"cacheIgnoredFiles" xml:"cacheIgnoredFiles" default:"false"`
ProgressUpdateIntervalS int `json:"progressUpdateIntervalS" xml:"progressUpdateIntervalS" default:"5"`
LimitBandwidthInLan bool `json:"limitBandwidthInLan" xml:"limitBandwidthInLan" default:"false"`
MinHomeDiskFree Size `json:"minHomeDiskFree" xml:"minHomeDiskFree" default:"1 %"`
-1
View File
@@ -42,7 +42,6 @@
<restartOnWakeup>true</restartOnWakeup>
<autoUpgradeIntervalH>0</autoUpgradeIntervalH>
<keepTemporariesH>24</keepTemporariesH>
<cacheIgnoredFiles>true</cacheIgnoredFiles>
<progressUpdateIntervalS>5</progressUpdateIntervalS>
<symlinksEnabled>true</symlinksEnabled>
<limitBandwidthInLan>false</limitBandwidthInLan>
-1
View File
@@ -22,7 +22,6 @@
<restartOnWakeup>false</restartOnWakeup>
<autoUpgradeIntervalH>24</autoUpgradeIntervalH>
<keepTemporariesH>48</keepTemporariesH>
<cacheIgnoredFiles>true</cacheIgnoredFiles>
<progressUpdateIntervalS>10</progressUpdateIntervalS>
<symlinksEnabled>false</symlinksEnabled>
<limitBandwidthInLan>true</limitBandwidthInLan>
+5
View File
@@ -827,6 +827,11 @@ func (s *service) logListenAddressesChangedEvent(l ListenerAddresses) {
func (s *service) CommitConfiguration(from, to config.Configuration) bool {
newDevices := make(map[protocol.DeviceID]bool, len(to.Devices))
for _, dev := range to.Devices {
if dev.DeviceID == s.myID {
// Do not report connection metrics for ourselves
continue
}
newDevices[dev.DeviceID] = true
registerDeviceMetrics(dev.DeviceID.String())
}
+5 -3
View File
@@ -13,6 +13,7 @@ import (
"log/slog"
"net"
"net/url"
"slices"
"sync"
"time"
@@ -185,9 +186,10 @@ func (t *tcpListener) WANAddresses() []*url.URL {
t.mut.RUnlock()
// If we support ReusePort, add an unspecified zero port address, which will be resolved by the discovery server
// in hopes that TCP punch through works.
if dialer.SupportsReusePort {
// If we support ReusePort, and we are already announcing an unspecified
// address, add an unspecified zero port address, which will be resolved
// by the discovery server in hopes that TCP punch through works.
if dialer.SupportsReusePort && slices.ContainsFunc(uris, func(u *url.URL) bool { return u.Hostname() == "0.0.0.0" }) {
uri := *t.uri
uri.Host = "0.0.0.0:0"
uris = append([]*url.URL{&uri}, uris...)
+2 -1
View File
@@ -289,7 +289,6 @@ func (c *globalClient) sendAnnouncement(ctx context.Context, timer *time.Timer)
resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode > 299 {
slog.DebugContext(ctx, "announce POST", "server", c.server, "status", resp.Status)
c.setError(errors.New(resp.Status))
if h := resp.Header.Get("Retry-After"); h != "" {
@@ -408,6 +407,7 @@ func (c *idCheckingHTTPClient) Get(ctx context.Context, url string) (*http.Respo
return nil, err
}
if err := c.check(resp); err != nil {
resp.Body.Close()
return nil, err
}
@@ -420,6 +420,7 @@ func (c *idCheckingHTTPClient) Post(ctx context.Context, url, ctype string, data
return nil, err
}
if err := c.check(resp); err != nil {
resp.Body.Close()
return nil, err
}
+14 -19
View File
@@ -174,7 +174,7 @@ func (f *BasicFilesystem) MkdirAll(path string, perm FileMode) error {
return err
}
return f.mkdirAll(path, os.FileMode(perm))
return os.MkdirAll(path, os.FileMode(perm))
}
func (f *BasicFilesystem) Lstat(name string) (FileInfo, error) {
@@ -240,23 +240,24 @@ func (f *BasicFilesystem) DirNames(name string) ([]string, error) {
return names, nil
}
// Open opens the file for reading, while not following symlinks
func (f *BasicFilesystem) Open(name string) (File, error) {
rootedName, err := f.rooted(name)
if err != nil {
return nil, err
}
fd, err := os.Open(rootedName)
if err != nil {
return nil, err
}
return basicFile{fd, name}, err
return f.OpenFile(name, os.O_RDONLY, 0)
}
// OpenFile is the generalised file open call, using the flags to determine
// the open semantics. We always add O_NOFOLLOW, disallowing opening files
// by following symlinks, unless OptFollow is set.
func (f *BasicFilesystem) OpenFile(name string, flags int, mode FileMode) (File, error) {
rootedName, err := f.rooted(name)
if err != nil {
return nil, err
}
if flags&OptFollow != 0 {
flags &^= OptFollow // unset the synthetic OptFollow bit
} else {
flags |= optNoFollow
}
fd, err := os.OpenFile(rootedName, flags, os.FileMode(mode))
if err != nil {
return nil, err
@@ -264,16 +265,10 @@ func (f *BasicFilesystem) OpenFile(name string, flags int, mode FileMode) (File,
return basicFile{fd, name}, err
}
// Create opens a file for writing, creating it as required. The Create will
// fail if the destination is a symlink.
func (f *BasicFilesystem) Create(name string) (File, error) {
rootedName, err := f.rooted(name)
if err != nil {
return nil, err
}
fd, err := os.Create(rootedName)
if err != nil {
return nil, err
}
return basicFile{fd, name}, err
return f.OpenFile(name, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o666)
}
func (*BasicFilesystem) Walk(_ string, _ WalkFunc) error {
+3 -4
View File
@@ -14,8 +14,11 @@ import (
"path/filepath"
"strconv"
"strings"
"syscall"
)
const optNoFollow = syscall.O_NOFOLLOW
func (f *BasicFilesystem) CreateSymlink(target, name string) error {
name, err := f.rooted(name)
if err != nil {
@@ -32,10 +35,6 @@ func (f *BasicFilesystem) ReadSymlink(name string) (string, error) {
return os.Readlink(name)
}
func (*BasicFilesystem) mkdirAll(path string, perm os.FileMode) error {
return os.MkdirAll(path, perm)
}
// Unhide is a noop on unix, as unhiding files requires renaming them.
// We still check that the relative path does not try to escape the root
func (f *BasicFilesystem) Unhide(name string) error {
+1 -2
View File
@@ -4,11 +4,10 @@
// 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) && !(darwin && kqueue) && !(android && amd64)
//go:build !(solaris && !cgo) && !(darwin && !cgo) && !(darwin && kqueue)
// +build !solaris cgo
// +build !darwin cgo
// +build !darwin !kqueue
// +build !android !amd64
package fs
+2 -2
View File
@@ -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) || (darwin && kqueue)
// +build solaris,!cgo darwin,!cgo android,amd64 darwin,kqueue
//go:build (solaris && !cgo) || (darwin && !cgo) || (darwin && kqueue)
// +build solaris,!cgo darwin,!cgo darwin,kqueue
package fs
+28 -104
View File
@@ -5,22 +5,22 @@
// You can obtain one at https://mozilla.org/MPL/2.0/.
//go:build windows
// +build windows
package fs
import (
"bytes"
"errors"
"math/bits"
"os"
"path/filepath"
"strings"
"syscall"
"unsafe"
"golang.org/x/sys/windows"
)
const optNoFollow = 0 // not defined for Windows
var errNotSupported = errors.New("symlinks not supported")
func (BasicFilesystem) ReadSymlink(path string) (string, error) {
@@ -31,57 +31,6 @@ func (BasicFilesystem) CreateSymlink(target, name string) error {
return errNotSupported
}
// Required due to https://github.com/golang/go/issues/10900
func (f *BasicFilesystem) mkdirAll(path string, perm os.FileMode) error {
// Fast path: if we can tell whether path is a directory or file, stop with success or error.
dir, err := os.Stat(path)
if err == nil {
if dir.IsDir() {
return nil
}
return &os.PathError{
Op: "mkdir",
Path: path,
Err: syscall.ENOTDIR,
}
}
// Slow path: make sure parent exists and then call Mkdir for path.
i := len(path)
for i > 0 && IsPathSeparator(path[i-1]) { // Skip trailing path separator.
i--
}
j := i
for j > 0 && !IsPathSeparator(path[j-1]) { // Scan backward over element.
j--
}
if j > 1 {
// Create parent
parent := path[0 : j-1]
if parent != filepath.VolumeName(parent) {
err = f.mkdirAll(parent, perm)
if err != nil {
return err
}
}
}
// Parent now exists; invoke Mkdir and use its result.
err = os.Mkdir(path, perm)
if err != nil {
// Handle arguments like "foo/." by
// double-checking that directory doesn't exist.
dir, err1 := os.Lstat(path)
if err1 == nil && dir.IsDir() {
return nil
}
return err
}
return nil
}
func (f *BasicFilesystem) Unhide(name string) error {
name, err := f.rooted(name)
if err != nil {
@@ -121,30 +70,17 @@ func (f *BasicFilesystem) Hide(name string) error {
}
func (f *BasicFilesystem) Roots() ([]string, error) {
kernel32, err := syscall.LoadDLL("kernel32.dll")
if err != nil {
return nil, err
}
getLogicalDriveStringsHandle, err := kernel32.FindProc("GetLogicalDriveStringsA")
mask, err := windows.GetLogicalDrives()
if err != nil {
return nil, err
}
buffer := [1024]byte{}
bufferSize := uint32(len(buffer))
hr, _, _ := getLogicalDriveStringsHandle.Call(uintptr(unsafe.Pointer(&bufferSize)), uintptr(unsafe.Pointer(&buffer)))
if hr == 0 {
return nil, errors.New("syscall failed")
}
var drives []string
parts := bytes.Split(buffer[:], []byte{0})
for _, part := range parts {
if len(part) == 0 {
break
drives := make([]string, 0, bits.OnesCount32(mask))
for letter := byte('A'); mask != 0; letter++ {
if mask&1 == 1 {
drives = append(drives, string([]byte{letter, ':', '\\'}))
}
drives = append(drives, string(part))
mask >>= 1
}
return drives, nil
@@ -168,14 +104,14 @@ func (f *BasicFilesystem) Lchown(name, uid, gid string) error {
// SetSecurityInfo.
var si windows.SECURITY_INFORMATION
var ownerSID, groupSID *syscall.SID
var ownerSID, groupSID *windows.SID
if uid != "" {
ownerSID, err = syscall.StringToSid(uid)
ownerSID, err = windows.StringToSid(uid)
if err == nil {
si |= windows.OWNER_SECURITY_INFORMATION
}
} else if gid != "" {
groupSID, err = syscall.StringToSid(uid)
groupSID, err = windows.StringToSid(uid)
if err == nil {
si |= windows.GROUP_SECURITY_INFORMATION
}
@@ -183,7 +119,7 @@ func (f *BasicFilesystem) Lchown(name, uid, gid string) error {
return errors.New("neither uid nor gid specified")
}
return windows.SetSecurityInfo(hdl, windows.SE_FILE_OBJECT, si, (*windows.SID)(ownerSID), (*windows.SID)(groupSID), nil, nil)
return windows.SetSecurityInfo(hdl, windows.SE_FILE_OBJECT, si, ownerSID, groupSID, nil, nil)
}
func (f *BasicFilesystem) Remove(name string) error {
@@ -267,47 +203,35 @@ func getFinalPathName(in string) (string, error) {
// Wrap the call to GetFinalPathNameByHandleW
// The string returned by this function uses the \?\ syntax
// Implies GetFullPathName + GetLongPathName
kernel32, err := syscall.LoadDLL("kernel32.dll")
if err != nil {
return "", err
}
GetFinalPathNameByHandleW, err := kernel32.FindProc("GetFinalPathNameByHandleW")
// https://github.com/golang/go/blob/ff048033e4304898245d843e79ed1a0897006c6d/src/internal/syscall/windows/syscall_windows.go#L303
if err != nil {
return "", err
}
inPath, err := syscall.UTF16PtrFromString(in)
if err != nil {
return "", err
}
// Get a file handler
h, err := syscall.CreateFile(inPath,
syscall.GENERIC_READ,
syscall.FILE_SHARE_READ,
// Get a file handle
h, err := windows.CreateFile(inPath,
windows.GENERIC_READ,
windows.FILE_SHARE_READ,
nil,
syscall.OPEN_EXISTING,
uint32(syscall.FILE_FLAG_BACKUP_SEMANTICS),
windows.OPEN_EXISTING,
windows.FILE_FLAG_BACKUP_SEMANTICS,
0)
if err != nil {
return "", err
}
defer syscall.CloseHandle(h)
// Call GetFinalPathNameByHandleW
var VOLUME_NAME_DOS uint32 = 0x0 // not yet defined in syscall
var bufSize uint32 = syscall.MAX_PATH // 260
defer windows.CloseHandle(h)
const VOLUME_NAME_DOS = 0x0 // not yet defined in x/sys/windows
var bufSize uint32 = windows.MAX_PATH
for i := 0; i < 2; i++ {
buf := make([]uint16, bufSize)
var ret uintptr
ret, _, err = GetFinalPathNameByHandleW.Call(
uintptr(h), // HANDLE hFile
uintptr(unsafe.Pointer(&buf[0])), // LPWSTR lpszFilePath
uintptr(bufSize), // DWORD cchFilePath
uintptr(VOLUME_NAME_DOS), // DWORD dwFlags
)
var ret uint32
ret, err = windows.GetFinalPathNameByHandle(
h, &buf[0], bufSize, VOLUME_NAME_DOS)
// The returned value is the actual length of the norm path
// After Win 10 build 1607, MAX_PATH limitations have been removed
// so it is necessary to check newBufSize
newBufSize := uint32(ret) + 1
newBufSize := ret + 1
if ret == 0 || newBufSize > bufSize*100 {
break
}
+1 -1
View File
@@ -499,7 +499,7 @@ func newCaseNode(name string, filesystem Filesystem) *caseNode {
lower := UnicodeLowercaseNormalized(n)
if lower != lastLower {
node.lowerToReal[lower] = n
lastLower = n
lastLower = lower
}
}
+2 -2
View File
@@ -153,7 +153,7 @@ func newFakeFilesystem(rootURI string, _ ...Option) *fakeFS {
for (files == 0 || createdFiles < files) && (maxsize == 0 || writtenData>>20 < int64(maxsize)) {
dir := filepath.Join(fmt.Sprintf("%02x", rng.Intn(255)), fmt.Sprintf("%02x", rng.Intn(255)))
file := fmt.Sprintf("%016x", rng.Int63())
_ = fs.MkdirAll(dir, 0o755)
_ = fs.MkdirAll(dir, ModePerm)
fd, _ := fs.Create(filepath.Join(dir, file))
createdFiles++
@@ -169,7 +169,7 @@ func newFakeFilesystem(rootURI string, _ ...Option) *fakeFS {
if !nostfolder {
// Also create a default folder marker for good measure
_ = fs.Mkdir(".stfolder", 0o700)
_ = fs.Mkdir(".stfolder", ModePerm)
}
// We only set the latency after doing the operations required to create
+11
View File
@@ -12,6 +12,7 @@ import (
"fmt"
"io"
"io/fs"
"math/bits"
"os"
"path/filepath"
"strings"
@@ -173,8 +174,18 @@ const (
OptSync = os.O_SYNC
OptTruncate = os.O_TRUNC
OptWriteOnly = os.O_WRONLY
OptFollow = 1 << (bits.UintSize - 2)
)
func init() {
// Ensure OptFollow doesn't clash with any other flag.
flags := OptAppend | OptCreate | OptExclusive | OptReadOnly | OptReadWrite | OptSync | OptTruncate | OptWriteOnly
flags &= OptFollow
if flags != 0 {
panic("bug: flag clash")
}
}
// SkipDir is used as a return value from WalkFuncs to indicate that
// the directory named in the call is to be skipped. It is not returned
// as an error by any function.
+26
View File
@@ -234,3 +234,29 @@ func TestRepro9677MissingMtimeFS(t *testing.T) {
newFS := NewFilesystem(FilesystemTypeFake, fmt.Sprintf("%v?insens=true&timeprecisionsecond=true", t.Name()), &OptionDetectCaseConflicts{}, NewMtimeOption(mtimeDB, ""))
checkMtime(newFS)
}
func TestBasicFilesystemRoots(t *testing.T) {
var bfs BasicFilesystem
roots, err := bfs.Roots()
if err != nil {
t.Fatal(err)
}
// The expected results depends on the system and we can't say too much,
// but do some sanity checking.
t.Logf("%d roots: %+v", len(roots), roots)
if len(roots) > 25 {
t.Errorf("suspiciously large number of filesystem roots: %d", len(roots))
}
rm := make(map[string]struct{})
for _, root := range roots {
if root == "" {
t.Error("empty root")
continue
}
if _, ok := rm[root]; ok {
t.Errorf("duplicate root %q", root)
continue
}
rm[root] = struct{}{}
}
}
-66
View File
@@ -1,66 +0,0 @@
// Copyright (C) 2014 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/.
package ignore
import (
"time"
"github.com/syncthing/syncthing/lib/ignore/ignoreresult"
)
type nower interface {
Now() time.Time
}
var clock = nower(defaultClock{})
type cache struct {
entries map[string]cacheEntry
}
type cacheEntry struct {
result ignoreresult.R
access int64 // Unix nanosecond count. Sufficient until the year 2262.
}
func newCache() *cache {
return &cache{
entries: make(map[string]cacheEntry),
}
}
func (c *cache) clean(d time.Duration) {
for k, v := range c.entries {
if clock.Now().Sub(time.Unix(0, v.access)) > d {
delete(c.entries, k)
}
}
}
func (c *cache) get(key string) (ignoreresult.R, bool) {
entry, ok := c.entries[key]
if ok {
entry.access = clock.Now().UnixNano()
c.entries[key] = entry
}
return entry.result, ok
}
func (c *cache) set(key string, result ignoreresult.R) {
c.entries[key] = cacheEntry{result, time.Now().UnixNano()}
}
func (c *cache) len() int {
l := len(c.entries)
return l
}
type defaultClock struct{}
func (defaultClock) Now() time.Time {
return time.Now()
}
-94
View File
@@ -1,94 +0,0 @@
// Copyright (C) 2014 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/.
package ignore
import (
"testing"
"time"
"github.com/syncthing/syncthing/lib/ignore/ignoreresult"
)
func TestCache(t *testing.T) {
fc := new(fakeClock)
oldClock := clock
clock = fc
defer func() {
clock = oldClock
}()
c := newCache()
res, ok := c.get("nonexistent")
if res.IsIgnored() || res.IsDeletable() || ok {
t.Errorf("res %v, ok %v for nonexistent item", res, ok)
}
// Set and check some items
c.set("true", ignoreresult.IgnoredDeletable)
c.set("false", 0)
res, ok = c.get("true")
if !res.IsIgnored() || !res.IsDeletable() || !ok {
t.Errorf("res %v, ok %v for true item", res, ok)
}
res, ok = c.get("false")
if res.IsIgnored() || res.IsDeletable() || !ok {
t.Errorf("res %v, ok %v for false item", res, ok)
}
// Don't clean anything
c.clean(time.Second)
// Same values should exist
res, ok = c.get("true")
if !res.IsIgnored() || !res.IsDeletable() || !ok {
t.Errorf("res %v, ok %v for true item", res, ok)
}
res, ok = c.get("false")
if res.IsIgnored() || res.IsDeletable() || !ok {
t.Errorf("res %v, ok %v for false item", res, ok)
}
// Sleep and access, to get some data for clean
*fc += 500 // milliseconds
c.get("true")
*fc += 100 // milliseconds
// "false" was accessed ~600 ms ago, "true" was accessed ~100 ms ago.
// This should clean out "false" but not "true"
c.clean(300 * time.Millisecond)
// Same values should exist
_, ok = c.get("true")
if !ok {
t.Error("item should still exist")
}
_, ok = c.get("false")
if ok {
t.Errorf("item should have been cleaned")
}
}
type fakeClock int64 // milliseconds
func (f *fakeClock) Now() time.Time {
t := time.Unix(int64(*f)/1000, (int64(*f)%1000)*int64(time.Millisecond))
*f++
return t
}
+9 -56
View File
@@ -92,7 +92,7 @@ func (p Pattern) allowsSkippingIgnoredDirs() bool {
if p.result.IsIgnored() {
return true
}
if p.pattern[0] != '/' {
if p.pattern == "" || p.pattern[0] != '/' {
return false
}
// A "/**" at the end is allowed and doesn't have any bearing on the
@@ -124,10 +124,7 @@ type Matcher struct {
fs fs.Filesystem
lines []string // exact lines read from .stignore
patterns []Pattern // patterns including those from included files
withCache bool
matches *cache
curHash string
stop chan struct{}
changeDetector ChangeDetector
mut sync.Mutex
}
@@ -135,13 +132,6 @@ type Matcher struct {
// An Option can be passed to New()
type Option func(*Matcher)
// WithCache enables or disables lookup caching. The default is disabled.
func WithCache(v bool) Option {
return func(m *Matcher) {
m.withCache = v
}
}
// WithChangeDetector sets a custom ChangeDetector. The default is to simply
// use the on disk modtime for comparison.
func WithChangeDetector(cd ChangeDetector) Option {
@@ -152,8 +142,7 @@ func WithChangeDetector(cd ChangeDetector) Option {
func New(fs fs.Filesystem, opts ...Option) *Matcher {
m := &Matcher{
fs: fs,
stop: make(chan struct{}),
fs: fs,
}
for _, opt := range opts {
opt(m)
@@ -161,9 +150,6 @@ func New(fs fs.Filesystem, opts ...Option) *Matcher {
if m.changeDetector == nil {
m.changeDetector = newModtimeChecker()
}
if m.withCache {
go m.clean(2 * time.Hour)
}
return m
}
@@ -219,9 +205,6 @@ func (m *Matcher) parseLocked(r io.Reader, file string) error {
m.curHash = newHash
m.patterns = patterns
if m.withCache {
m.matches = newCache()
}
return err
}
@@ -232,7 +215,7 @@ func (m *Matcher) parseLocked(r io.Reader, file string) error {
// NFC everywhere else). This is always the case in real usage in syncthing, as
// we ensure native unicode normalisation on all entry points (scanning and from
// protocol) - so no need to normalize when calling this, except e.g. in tests.
func (m *Matcher) Match(file string) (result ignoreresult.R) {
func (m *Matcher) Match(file string) ignoreresult.R {
switch {
case fs.IsTemporary(file):
return ignoreresult.IgnoreAndSkip
@@ -254,19 +237,6 @@ func (m *Matcher) Match(file string) (result ignoreresult.R) {
// Change backslashes to slashes (on Windows only)
file = filepath.ToSlash(file)
if m.matches != nil {
// Check the cache for a known result.
res, ok := m.matches.get(file)
if ok {
return res
}
// Update the cache with the result at return time
defer func() {
m.matches.set(file, result)
}()
}
// Check all the patterns for a match. Track whether the patterns so far
// allow skipping matched directories or not. As soon as we hit an
// exclude pattern (with some exceptions), we can't skip directories
@@ -327,27 +297,6 @@ func (m *Matcher) Hash() string {
return m.curHash
}
func (m *Matcher) Stop() {
close(m.stop)
}
func (m *Matcher) clean(d time.Duration) {
t := time.NewTimer(d / 2)
for {
select {
case <-m.stop:
return
case <-t.C:
m.mut.Lock()
if m.matches != nil {
m.matches.clean(d)
}
t.Reset(d / 2)
m.mut.Unlock()
}
}
}
func hashPatterns(patterns []Pattern) string {
h := sha256.New()
for _, pat := range patterns {
@@ -357,8 +306,8 @@ func hashPatterns(patterns []Pattern) string {
return hex.EncodeToString(h.Sum(nil))
}
func loadIgnoreFile(fs fs.Filesystem, file string) (fs.File, fs.FileInfo, error) {
fd, err := fs.Open(file)
func loadIgnoreFile(ffs fs.Filesystem, file string) (fs.File, fs.FileInfo, error) {
fd, err := ffs.OpenFile(file, fs.OptReadOnly|fs.OptFollow, 0o666)
if err != nil {
return fd, nil, err
}
@@ -463,6 +412,10 @@ func parseLine(line string) ([]Pattern, error) {
patterns[0] = pattern
line = line[3:]
if line == "" {
// Pattern was exactly "**/", already covered by patterns[0].
return patterns[:1], nil
}
pattern.pattern = line
pattern.match, err = glob.Compile(line, '/')
if err != nil {
+77 -205
View File
@@ -52,7 +52,7 @@ func newTestFS() fs.Filesystem {
func TestIgnore(t *testing.T) {
testFs := newTestFS()
pats := New(testFs, WithCache(true))
pats := New(testFs)
err := pats.Load(".stignore")
if err != nil {
t.Fatal(err)
@@ -104,7 +104,7 @@ func TestExcludes(t *testing.T) {
i*2
!ign2
`
pats := New(testFs, WithCache(true))
pats := New(testFs)
err := pats.Parse(bytes.NewBufferString(stignore), ".stignore")
if err != nil {
t.Fatal(err)
@@ -151,7 +151,7 @@ func TestFlagOrder(t *testing.T) {
(?i)(?d)(?d)!ign9
(?d)(?d)!ign10
`
pats := New(testFs, WithCache(true))
pats := New(testFs)
err := pats.Parse(bytes.NewBufferString(stignore), ".stignore")
if err != nil {
t.Fatal(err)
@@ -188,7 +188,7 @@ func TestDeletables(t *testing.T) {
ign7
(?i)ign8
`
pats := New(testFs, WithCache(true))
pats := New(testFs)
err := pats.Parse(bytes.NewBufferString(stignore), ".stignore")
if err != nil {
t.Fatal(err)
@@ -229,7 +229,7 @@ func TestBadPatterns(t *testing.T) {
}
for _, pat := range badPatterns {
err := New(testFs, WithCache(true)).Parse(bytes.NewBufferString(pat), ".stignore")
err := New(testFs).Parse(bytes.NewBufferString(pat), ".stignore")
if err == nil {
t.Errorf("No error for pattern %q", pat)
}
@@ -247,7 +247,7 @@ func TestBadPatterns(t *testing.T) {
func TestCaseSensitivity(t *testing.T) {
testFs := newTestFS()
ign := New(testFs, WithCache(true))
ign := New(testFs)
err := ign.Parse(bytes.NewBufferString("test"), ".stignore")
if err != nil {
t.Error(err)
@@ -275,126 +275,6 @@ func TestCaseSensitivity(t *testing.T) {
}
}
func TestCaching(t *testing.T) {
fs := fs.NewFilesystem(fs.FilesystemTypeFake, rand.String(32)+"?content=true")
fd1, err := osutil.TempFile(fs, "", "")
if err != nil {
t.Fatal(err)
}
fd2, err := osutil.TempFile(fs, "", "")
if err != nil {
t.Fatal(err)
}
defer fd1.Close()
defer fd2.Close()
defer fs.Remove(fd1.Name())
defer fs.Remove(fd2.Name())
_, err = fd1.Write([]byte("/x/\n#include " + filepath.Base(fd2.Name()) + "\n"))
if err != nil {
t.Fatal(err)
}
fd2.Write([]byte("/y/\n"))
pats := New(fs, WithCache(true))
err = pats.Load(fd1.Name())
if err != nil {
t.Fatal(err)
}
if pats.matches.len() != 0 {
t.Fatal("Expected empty cache")
}
// Cache some outcomes
for _, letter := range []string{"a", "b", "x", "y"} {
pats.Match(letter)
}
if pats.matches.len() != 4 {
t.Fatal("Expected 4 cached results")
}
// Reload file, expect old outcomes to be preserved
err = pats.Load(fd1.Name())
if err != nil {
t.Fatal(err)
}
if pats.matches.len() != 4 {
t.Fatal("Expected 4 cached results")
}
// Modify the include file, expect empty cache. Ensure the timestamp on
// the file changes.
fd2.Write([]byte("/z/\n"))
fd2.Sync()
fakeTime := time.Now().Add(5 * time.Second)
fs.Chtimes(fd2.Name(), fakeTime, fakeTime)
err = pats.Load(fd1.Name())
if err != nil {
t.Fatal(err)
}
if pats.matches.len() != 0 {
t.Fatal("Expected 0 cached results")
}
// Cache some outcomes again
for _, letter := range []string{"b", "x", "y"} {
pats.Match(letter)
}
// Verify that outcomes preserved on next load
err = pats.Load(fd1.Name())
if err != nil {
t.Fatal(err)
}
if pats.matches.len() != 3 {
t.Fatal("Expected 3 cached results")
}
// Modify the root file, expect cache to be invalidated
fd1.Write([]byte("/a/\n"))
fd1.Sync()
fakeTime = time.Now().Add(5 * time.Second)
fs.Chtimes(fd1.Name(), fakeTime, fakeTime)
err = pats.Load(fd1.Name())
if err != nil {
t.Fatal(err)
}
if pats.matches.len() != 0 {
t.Fatal("Expected cache invalidation")
}
// Cache some outcomes again
for _, letter := range []string{"b", "x", "y"} {
pats.Match(letter)
}
// Verify that outcomes provided on next load
err = pats.Load(fd1.Name())
if err != nil {
t.Fatal(err)
}
if pats.matches.len() != 3 {
t.Fatal("Expected 3 cached results")
}
}
func TestCommentsAndBlankLines(t *testing.T) {
testFs := newTestFS()
@@ -409,7 +289,7 @@ func TestCommentsAndBlankLines(t *testing.T) {
`
pats := New(testFs, WithCache(true))
pats := New(testFs)
err := pats.Parse(bytes.NewBufferString(stignore), ".stignore")
if err != nil {
t.Error(err)
@@ -419,6 +299,17 @@ func TestCommentsAndBlankLines(t *testing.T) {
}
}
func TestNegatedGlobstarOnly(t *testing.T) {
testFs := newTestFS()
pats := New(testFs)
if err := pats.Parse(bytes.NewBufferString("!**/\n"), ".stignore"); err != nil {
t.Fatal(err)
}
// Must not panic on an empty pattern component.
pats.Match("foo")
}
var result ignoreresult.R
func BenchmarkMatch(b *testing.B) {
@@ -451,59 +342,6 @@ flamingo
}
}
func BenchmarkMatchCached(b *testing.B) {
stignore := `
.frog
.frog*
.frogfox
.whale
.whale/*
.dolphin
.dolphin/*
~ferret~.*
.ferret.*
flamingo.*
flamingo
*.crow
*.crow
`
// Caches per file, hence write the patterns to a file.
fs := fs.NewFilesystem(fs.FilesystemTypeFake, rand.String(32)+"?content=true")
fd, err := osutil.TempFile(fs, "", "")
if err != nil {
b.Fatal(err)
}
_, err = fd.Write([]byte(stignore))
defer fd.Close()
defer fs.Remove(fd.Name())
if err != nil {
b.Fatal(err)
}
// Load the patterns
pats := New(fs, WithCache(true))
err = pats.Load(fd.Name())
if err != nil {
b.Fatal(err)
}
// Cache the outcome for "filename"
pats.Match("filename")
// This load should now load the cached outcomes as the set of patterns
// has not changed.
err = pats.Load(fd.Name())
if err != nil {
b.Fatal(err)
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
result = pats.Match("filename")
}
}
func TestCacheReload(t *testing.T) {
fs := fs.NewFilesystem(fs.FilesystemTypeFake, rand.String(32)+"?content=true")
@@ -522,7 +360,7 @@ func TestCacheReload(t *testing.T) {
t.Fatal(err)
}
pats := New(fs, WithCache(true))
pats := New(fs)
err = pats.Load(fd.Name())
if err != nil {
t.Fatal(err)
@@ -579,7 +417,7 @@ func TestCacheReload(t *testing.T) {
func TestHash(t *testing.T) {
testFs := newTestFS()
p1 := New(testFs, WithCache(true))
p1 := New(testFs)
err := p1.Load(".stignore")
if err != nil {
t.Fatal(err)
@@ -595,7 +433,7 @@ func TestHash(t *testing.T) {
/ffile
lost+found
`
p2 := New(testFs, WithCache(true))
p2 := New(testFs)
err = p2.Parse(bytes.NewBufferString(stignore), ".stignore")
if err != nil {
t.Fatal(err)
@@ -610,7 +448,7 @@ func TestHash(t *testing.T) {
/ffile
lost+found
`
p3 := New(testFs, WithCache(true))
p3 := New(testFs)
err = p3.Parse(bytes.NewBufferString(stignore), ".stignore")
if err != nil {
t.Fatal(err)
@@ -636,7 +474,7 @@ func TestHash(t *testing.T) {
func TestHashOfEmpty(t *testing.T) {
testFs := newTestFS()
p1 := New(testFs, WithCache(true))
p1 := New(testFs)
err := p1.Load(".stignore")
if err != nil {
@@ -678,7 +516,7 @@ func TestWindowsPatterns(t *testing.T) {
a/b
c\d
`
pats := New(testFs, WithCache(true))
pats := New(testFs)
err := pats.Parse(bytes.NewBufferString(stignore), ".stignore")
if err != nil {
@@ -706,7 +544,7 @@ func TestAutomaticCaseInsensitivity(t *testing.T) {
A/B
c/d
`
pats := New(testFs, WithCache(true))
pats := New(testFs)
err := pats.Parse(bytes.NewBufferString(stignore), ".stignore")
if err != nil {
@@ -728,7 +566,7 @@ func TestCommas(t *testing.T) {
foo,bar.txt
{baz,quux}.txt
`
pats := New(testFs, WithCache(true))
pats := New(testFs)
err := pats.Parse(bytes.NewBufferString(stignore), ".stignore")
if err != nil {
@@ -762,7 +600,7 @@ func TestIssue3164(t *testing.T) {
(?d)(?i)/foo
(?d)(?i)**/bar
`
pats := New(testFs, WithCache(true))
pats := New(testFs)
err := pats.Parse(bytes.NewBufferString(stignore), ".stignore")
if err != nil {
@@ -801,7 +639,7 @@ func TestIssue3174(t *testing.T) {
stignore := `
*ä*
`
pats := New(testFs, WithCache(true))
pats := New(testFs)
err := pats.Parse(bytes.NewBufferString(stignore), ".stignore")
if err != nil {
@@ -822,7 +660,7 @@ func TestIssue3639(t *testing.T) {
stignore := `
foo/
`
pats := New(testFs, WithCache(true))
pats := New(testFs)
err := pats.Parse(bytes.NewBufferString(stignore), ".stignore")
if err != nil {
@@ -858,7 +696,7 @@ func TestIssue3674(t *testing.T) {
{"as/dc", true},
}
pats := New(testFs, WithCache(true))
pats := New(testFs)
err := pats.Parse(bytes.NewBufferString(stignore), ".stignore")
if err != nil {
@@ -893,7 +731,7 @@ func TestGobwasGlobIssue18(t *testing.T) {
{"bbaa", false},
}
pats := New(testFs, WithCache(true))
pats := New(testFs)
err := pats.Parse(bytes.NewBufferString(stignore), ".stignore")
if err != nil {
@@ -925,7 +763,7 @@ func TestRoot(t *testing.T) {
{"b", true},
}
pats := New(testFs, WithCache(true))
pats := New(testFs)
err := pats.Parse(bytes.NewBufferString(stignore), ".stignore")
if err != nil {
@@ -951,7 +789,7 @@ func TestLines(t *testing.T) {
!/a
`
pats := New(testFs, WithCache(true))
pats := New(testFs)
err := pats.Parse(bytes.NewBufferString(stignore), ".stignore")
if err != nil {
@@ -992,7 +830,7 @@ func TestDuplicateLines(t *testing.T) {
/*
`
pats := New(testFs, WithCache(true))
pats := New(testFs)
err := pats.Parse(bytes.NewBufferString(stignore), ".stignore")
if err != nil {
@@ -1025,7 +863,7 @@ func TestIssue4680(t *testing.T) {
{"#snapshot/foo", true},
}
pats := New(testFs, WithCache(true))
pats := New(testFs)
err := pats.Parse(bytes.NewBufferString(stignore), ".stignore")
if err != nil {
@@ -1045,7 +883,7 @@ func TestIssue4689(t *testing.T) {
stignore := `// orig`
pats := New(testFs, WithCache(true))
pats := New(testFs)
err := pats.Parse(bytes.NewBufferString(stignore), ".stignore")
if err != nil {
@@ -1076,7 +914,7 @@ func TestIssue4901(t *testing.T) {
puppy
`
pats := New(testFs, WithCache(true))
pats := New(testFs)
fd, err := pats.fs.Create(".stignore")
if err != nil {
@@ -1119,7 +957,7 @@ func TestIssue4901(t *testing.T) {
func TestIssue5009(t *testing.T) {
testFs := newTestFS()
pats := New(testFs, WithCache(true))
pats := New(testFs)
stignore := `
ign1
@@ -1152,7 +990,7 @@ func TestIssue5009(t *testing.T) {
func TestSpecialChars(t *testing.T) {
testFs := newTestFS()
pats := New(testFs, WithCache(true))
pats := New(testFs)
stignore := `(?i)/#recycle
(?i)/#nosync
@@ -1179,7 +1017,7 @@ func TestSpecialChars(t *testing.T) {
func TestIntlWildcards(t *testing.T) {
testFs := newTestFS()
pats := New(testFs, WithCache(true))
pats := New(testFs)
stignore := `1000
200?
@@ -1208,7 +1046,7 @@ func TestPartialIncludeLine(t *testing.T) {
// Loading a partial #include line (no file mentioned) should error but not crash.
pats := New(testFs, WithCache(true))
pats := New(testFs)
cases := []string{
"#include",
@@ -1267,7 +1105,7 @@ func TestSkipIgnoredDirs(t *testing.T) {
}
}
pats := New(testFs, WithCache(true))
pats := New(testFs)
stignore := `
/foo/ign*
@@ -1711,7 +1549,7 @@ func testEscape(t *testing.T, tests []escapeTest, noErrors bool) {
for name, content := range testEscapeFiles {
fs.WriteFile(testFS, name, []byte(content), 0o666)
}
pats := New(testFS, WithCache(true))
pats := New(testFS)
err := pats.Parse(bytes.NewBufferString(test.pattern), ".stignore")
if noErrors {
@@ -1731,3 +1569,37 @@ func testEscape(t *testing.T, tests []escapeTest, noErrors bool) {
}
}
}
// TestIgnoreThroughSymlink verifies that an ignore file can be loaded when
// it is itself a symlink pointing at the real file. This exercises the
// SkipDefaultOpenFlags path, as our normal Open enforces O_NOFOLLOW. We use
// a real filesystem here because the fake one does not implement the
// O_NOFOLLOW semantics that make this distinction meaningful.
func TestIgnoreThroughSymlink(t *testing.T) {
if build.IsWindows {
t.Skip("symlinks not supported on Windows")
}
testFS := fs.NewFilesystem(fs.FilesystemTypeBasic, t.TempDir())
// The real ignore file lives under a different name, and .stignore is a
// symlink pointing at it.
if err := fs.WriteFile(testFS, "real-ignores", []byte("bfile\n"), 0o666); err != nil {
t.Fatal(err)
}
if err := testFS.CreateSymlink("real-ignores", ".stignore"); err != nil {
t.Fatal(err)
}
pats := New(testFS)
if err := pats.Load(".stignore"); err != nil {
t.Fatal(err)
}
if !pats.Match("bfile").IsIgnored() {
t.Error("bfile should be ignored")
}
if pats.Match("afile").IsIgnored() {
t.Error("afile should not be ignored")
}
}
+7 -12
View File
@@ -8,20 +8,19 @@ package model
import (
"github.com/syncthing/syncthing/lib/protocol"
"google.golang.org/protobuf/proto"
)
// How many files to send in each Index/IndexUpdate message.
// How many files & blocks to send in each Index/IndexUpdate message.
const (
MaxBatchSizeBytes = 250 * 1024 // Aim for making index messages no larger than 250 KiB (uncompressed)
MaxBatchSizeFiles = 1000 // Either way, don't include more files than this
MaxBatchSizeFiles = 1000
MaxBatchSizeBlocks = 5000
)
// FileInfoBatch is a utility to do file operations on the database in suitably
// sized batches.
type FileInfoBatch struct {
infos []protocol.FileInfo
size int
nblocks int
flushFn func([]protocol.FileInfo) error
error error
}
@@ -47,11 +46,11 @@ func (b *FileInfoBatch) Append(f protocol.FileInfo) {
b.infos = make([]protocol.FileInfo, 0, MaxBatchSizeFiles)
}
b.infos = append(b.infos, f)
b.size += proto.Size(f.ToWire(true))
b.nblocks += len(f.Blocks)
}
func (b *FileInfoBatch) Full() bool {
return len(b.infos) >= MaxBatchSizeFiles || b.size >= MaxBatchSizeBytes
return len(b.infos) >= MaxBatchSizeFiles || b.nblocks >= MaxBatchSizeBlocks
}
func (b *FileInfoBatch) FlushIfFull() error {
@@ -82,9 +81,5 @@ func (b *FileInfoBatch) Flush() error {
func (b *FileInfoBatch) Reset() {
b.infos = nil
b.error = nil
b.size = 0
}
func (b *FileInfoBatch) Size() int {
return b.size
b.nblocks = 0
}
+74 -48
View File
@@ -195,19 +195,23 @@ func (f *folder) Serve(ctx context.Context) error {
case <-f.pullScheduled:
if f.PullerDelayS > 0 {
// Wait for incoming updates to settle before doing the
// actual pull. Only set the state to SyncWaiting if we have
// reason to believe there is something to sync, to avoid
// unnecessary flashing in the GUI.
if needCount, err := f.db.CountNeed(f.folderID, protocol.LocalDeviceID); err == nil && needCount.TotalItems() > 0 {
f.setState(FolderSyncWaiting)
// actual pull.
//
// Only set the state to SyncWaiting if we are Idle (and
// not, e.g., errored) and have reason to believe there is
// something to sync, to avoid unnecessary flashing in the
// GUI.
if cur, _, _ := f.getState(); cur == FolderIdle {
if needCount, err := f.db.CountNeed(f.folderID, protocol.LocalDeviceID); err == nil && needCount.TotalItems() > 0 {
f.setState(FolderSyncWaiting)
}
}
pullTimer.Reset(time.Duration(float64(time.Second) * f.PullerDelayS))
} else {
_, err = f.pull(ctx)
continue
}
_, err = f.pull(ctx)
case <-pullTimer.C:
f.setState(FolderIdle)
_, err = f.pull(ctx)
case <-f.pullFailTimer.C:
@@ -238,10 +242,12 @@ func (f *folder) Serve(ctx context.Context) error {
case next := <-f.scanDelay:
f.sl.DebugContext(ctx, "Delaying scan")
f.scanTimer.Reset(next)
continue
case <-f.scanScheduled:
f.sl.DebugContext(ctx, "Scan was scheduled")
f.scanTimer.Reset(0)
continue
case fsEvents := <-f.watchChan:
f.sl.DebugContext(ctx, "Scan due to watcher")
@@ -252,16 +258,20 @@ func (f *folder) Serve(ctx context.Context) error {
err = f.restartWatch(ctx)
case <-f.versionCleanupTimer.C:
if _, _, healthErr := f.getState(); healthErr != nil {
continue
}
f.sl.DebugContext(ctx, "Doing version cleanup")
f.versionCleanupTimerFired(ctx)
}
if err != nil {
if svcutil.IsFatal(err) {
return err
}
f.setError(ctx, err)
if svcutil.IsFatal(err) {
return err
}
// Set the state to FolderError when err is non-nil, otherwise reset
// it back to FolderIdle.
f.setError(ctx, err)
}
}
@@ -403,6 +413,15 @@ func (f *folder) pull(ctx context.Context) (success bool, err error) {
}
}()
// Abort early (before acquiring a limiter token) if there's a folder
// error. This must happen before the "nothing to do" check below so
// that up-to-date folders are also periodically health-checked.
err = f.getHealthErrorWithoutIgnores()
if err != nil {
f.sl.DebugContext(ctx, "Skipping pull due to folder error", slogutil.Error(err))
return false, err
}
// If there is nothing to do, don't even enter sync-waiting state.
needCount, err := f.db.CountNeed(f.folderID, protocol.LocalDeviceID)
if err != nil {
@@ -416,13 +435,6 @@ func (f *folder) pull(ctx context.Context) (success bool, err error) {
return true, nil
}
// Abort early (before acquiring a token) if there's a folder error
err = f.getHealthErrorWithoutIgnores()
if err != nil {
f.sl.DebugContext(ctx, "Skipping pull due to folder error", slogutil.Error(err))
return false, err
}
// Send only folder doesn't do any io, it only checks for out-of-sync
// items that differ in metadata and updates those.
if f.Type != config.FolderTypeSendOnly {
@@ -487,7 +499,6 @@ func (f *folder) scanSubdirs(ctx context.Context, subDirs []string) error {
}()
f.setState(FolderScanWaiting)
defer f.setState(FolderIdle)
if err := f.ioLimiter.TakeWithContext(ctx, 1); err != nil {
return err
@@ -571,17 +582,17 @@ func (f *folder) scanSubdirs(ctx context.Context, subDirs []string) error {
const maxToRemove = 1000
type scanBatch struct {
f *folder
updateBatch *FileInfoBatch
toRemove []string
deleted map[string]struct{}
f *folder
updateBatch *FileInfoBatch
toRemove []string
skipInFindRename map[string]struct{}
}
func (f *folder) newScanBatch() *scanBatch {
b := &scanBatch{
f: f,
toRemove: make([]string, 0, maxToRemove),
deleted: make(map[string]struct{}),
f: f,
toRemove: make([]string, 0, maxToRemove),
skipInFindRename: make(map[string]struct{}),
}
b.updateBatch = NewFileInfoBatch(func(fs []protocol.FileInfo) error {
if err := b.f.getHealthErrorWithoutIgnores(); err != nil {
@@ -589,7 +600,7 @@ func (f *folder) newScanBatch() *scanBatch {
return err
}
b.f.updateLocalsFromScanning(fs)
clear(b.deleted)
clear(b.skipInFindRename)
return nil
})
return b
@@ -625,12 +636,12 @@ func (b *scanBatch) FlushIfFull() error {
return b.updateBatch.FlushIfFull()
}
func (b *scanBatch) markDeleted(name string) {
b.deleted[name] = struct{}{}
func (b *scanBatch) markSkipInFindRename(name string) {
b.skipInFindRename[name] = struct{}{}
}
func (b *scanBatch) hasDeleted(name string) bool {
_, ok := b.deleted[name]
func (b *scanBatch) shouldSkipInFindRenames(name string) bool {
_, ok := b.skipInFindRename[name]
return ok
}
@@ -734,12 +745,18 @@ func (f *folder) scanSubdirsChangedAndNew(ctx context.Context, subDirs []string,
switch f.Type {
case config.FolderTypeReceiveOnly, config.FolderTypeReceiveEncrypted:
default:
if nf, ok := f.findRename(ctx, res.File, batch); ok {
if ok, err := batch.Update(nf); err != nil {
return 0, err
} else if ok {
changes++
batch.markDeleted(nf.Name)
// Rename detection is comparatively expensive, so only attempt
// it for files that appeared as new on disk during this scan. A
// rename that overwrites an existing file (the destination path
// already had an entry, so it scans as an update rather than a
// new file) is not optimised as a rename.
if res.File.New && res.File.Size > 0 {
if nf, ok := f.findRename(ctx, res.File, batch); ok {
if ok, err := batch.Update(nf); err != nil {
return 0, err
} else if ok {
changes++
}
}
}
}
@@ -932,7 +949,7 @@ loop:
continue
}
if batch.hasDeleted(fi.Name) {
if batch.shouldSkipInFindRenames(fi.Name) {
continue
}
@@ -952,6 +969,7 @@ loop:
}
if !osutil.IsDeleted(f.mtimefs, fi.Name) {
batch.markSkipInFindRename(fi.Name)
continue
}
@@ -963,6 +981,7 @@ loop:
nf.SetDeleted(f.shortID)
nf.LocalFlags = f.localFlags
found = true
batch.markSkipInFindRename(fi.Name)
break
}
@@ -975,10 +994,13 @@ func (f *folder) scanTimerFired(ctx context.Context) error {
select {
case <-f.initialScanFinished:
default:
if err != nil {
f.sl.ErrorContext(ctx, "Failed initial scan", slogutil.Error(err))
} else {
switch {
case err == nil:
f.sl.InfoContext(ctx, "Completed initial scan")
case errors.Is(err, context.Canceled):
// Suppressed: context was canceled (e.g. by user)
default:
f.sl.ErrorContext(ctx, "Failed initial scan", slogutil.Error(err))
}
close(f.initialScanFinished)
}
@@ -990,7 +1012,6 @@ func (f *folder) scanTimerFired(ctx context.Context) error {
func (f *folder) versionCleanupTimerFired(ctx context.Context) {
f.setState(FolderCleanWaiting)
defer f.setState(FolderIdle)
if err := f.ioLimiter.TakeWithContext(ctx, 1); err != nil {
return
@@ -1181,8 +1202,13 @@ func (f *folder) setError(ctx context.Context, err error) {
default:
}
_, _, oldErr := f.getState()
if (err != nil && oldErr != nil && oldErr.Error() == err.Error()) || (err == nil && oldErr == nil) {
state, _, oldErr := f.getState()
switch {
case err != nil && oldErr != nil && oldErr.Error() == err.Error():
// The error is the same as the old error, no change is required
return
case err == nil && oldErr == nil && state == FolderIdle:
// No error before or now, and we are already Idle
return
}
@@ -1192,7 +1218,7 @@ func (f *folder) setError(ctx context.Context, err error) {
} else {
f.sl.InfoContext(ctx, "Folder error changed", slogutil.Error(err), slog.Any("previously", oldErr))
}
} else {
} else if oldErr != nil {
f.sl.InfoContext(ctx, "Folder error cleared")
f.SchedulePull()
}
@@ -1200,7 +1226,7 @@ func (f *folder) setError(ctx context.Context, err error) {
if f.FSWatcherEnabled {
if err != nil {
f.stopWatch()
} else {
} else if oldErr != nil {
f.scheduleWatchRestart()
}
}
-1
View File
@@ -44,7 +44,6 @@ func (f *receiveEncryptedFolder) revert(ctx context.Context) error {
f.sl.InfoContext(ctx, "Reverting unexpected items")
f.setState(FolderScanning)
defer f.setState(FolderIdle)
batch := NewFileInfoBatch(func(fs []protocol.FileInfo) error {
f.updateLocalsFromScanning(fs)
-1
View File
@@ -74,7 +74,6 @@ func (f *receiveOnlyFolder) revert(ctx context.Context) error {
f.sl.InfoContext(ctx, "Reverting folder")
f.setState(FolderScanning)
defer f.setState(FolderIdle)
scanChan := make(chan string)
go f.pullScannerRoutine(ctx, scanChan)
+8 -7
View File
@@ -123,10 +123,12 @@ func TestRecvOnlyRevertNeeds(t *testing.T) {
oldData := []byte("hello\n")
knownFiles := setupKnownFiles(t, ffs, oldData)
// Send and index update for the known stuff
// Send an index update for the known stuff. Update the local index
// prior to getting index data from the remote device, so we avoid a
// pull starting and causing race flakiness for the rest of the test.
must(t, m.Index(conn, &protocol.Index{Folder: "ro", Files: knownFiles}))
f.updateLocalsFromScanning(knownFiles)
must(t, m.Index(conn, &protocol.Index{Folder: "ro", Files: knownFiles}))
// Scan the folder.
@@ -163,12 +165,11 @@ func TestRecvOnlyRevertNeeds(t *testing.T) {
// We now have a newer file than the rest of the cluster. Global state should reflect this.
size = mustV(m.GlobalSize("ro"))
const sizeOfDir = 128
if size.Files != 1 || size.Bytes != sizeOfDir+int64(len(oldData)) {
if size.Files != 1 || size.Bytes != int64(len(oldData)) {
t.Fatalf("Global: expected no change due to the new file: %+v", size)
}
size = mustV(m.LocalSize("ro", protocol.LocalDeviceID))
if size.Files != 1 || size.Bytes != sizeOfDir+int64(len(newData)) {
if size.Files != 1 || size.Bytes != int64(len(newData)) {
t.Fatalf("Local: expected the new file to be reflected: %+v", size)
}
size = mustV(m.NeedSize("ro", protocol.LocalDeviceID))
@@ -185,11 +186,11 @@ func TestRecvOnlyRevertNeeds(t *testing.T) {
m.Revert("ro")
size = mustV(m.GlobalSize("ro"))
if size.Files != 1 || size.Bytes != sizeOfDir+int64(len(oldData)) {
if size.Files != 1 || size.Bytes != int64(len(oldData)) {
t.Fatalf("Global: expected the global size to revert: %+v", size)
}
size = mustV(m.LocalSize("ro", protocol.LocalDeviceID))
if size.Files != 1 || size.Bytes != sizeOfDir+int64(len(newData)) {
if size.Files != 1 || size.Bytes != int64(len(newData)) {
t.Fatalf("Local: expected the local size to remain: %+v", size)
}
size = mustV(m.NeedSize("ro", protocol.LocalDeviceID))
-1
View File
@@ -98,7 +98,6 @@ func (f *sendOnlyFolder) override(ctx context.Context) error {
f.sl.InfoContext(ctx, "Overriding global state ")
f.setState(FolderScanning)
defer f.setState(FolderIdle)
batch := NewFileInfoBatch(func(files []protocol.FileInfo) error {
f.updateLocalsFromScanning(files)
+15 -3
View File
@@ -168,7 +168,6 @@ func (f *sendReceiveFolder) pull(ctx context.Context) (bool, error) {
go f.pullScannerRoutine(ctx, scanChan)
defer func() {
close(scanChan)
f.setState(FolderIdle)
}()
metricFolderPulls.WithLabelValues(f.ID).Inc()
@@ -382,7 +381,7 @@ loop:
if err != nil {
return nil, nil, err
}
if hasCurFile && file.BlocksEqual(curFile) {
if hasCurFile && curFile.Type == file.Type && file.BlocksEqual(curFile) {
// We are supposed to copy the entire file, and then fetch nothing. We
// are only updating metadata, so we don't actually *need* to make the
// copy.
@@ -701,7 +700,7 @@ func (f *sendReceiveFolder) checkParent(file string, scanChan chan<- string) boo
return true
}
f.sl.Debug("Creating parent directory", slogutil.FilePath(file))
if err := f.mtimefs.MkdirAll(parent, 0o755); err != nil {
if err := f.mtimefs.MkdirAll(parent, fs.ModePerm); err != nil {
f.newPullError(file, fmt.Errorf("creating parent dir: %w", err))
return false
}
@@ -1345,6 +1344,12 @@ func (f *sendReceiveFolder) copierRoutine(ctx context.Context, in <-chan copyBlo
default:
}
if block.Size == 0 {
// Copying zero bytes is a no-op.
state.copyDone(block)
continue
}
if !f.DisableSparseFiles && state.reused == 0 && block.IsEmpty() {
// The block is a block of all zeroes, and we are not reusing
// a temp file, so there is no need to do anything with it.
@@ -1534,6 +1539,13 @@ func (f *sendReceiveFolder) pullerRoutine(ctx context.Context, in <-chan pullBlo
bytes := state.block.Size
if bytes == 0 {
// Pulling zero bytes is a no-op.
state.pullDone(state.block)
out <- state.sharedPullerState
continue
}
if err := requestLimiter.TakeWithContext(ctx, bytes); err != nil {
state.fail(err)
out <- state.sharedPullerState
+1 -1
View File
@@ -891,7 +891,7 @@ func TestPullCtxCancel(t *testing.T) {
emptyState := func() pullBlockState {
return pullBlockState{
sharedPullerState: newSharedPullerState(protocol.FileInfo{}, nil, f.folderID, "", nil, nil, false, false, protocol.FileInfo{}, false, false),
block: protocol.BlockInfo{},
block: protocol.BlockInfo{Size: 42},
}
}
+3 -3
View File
@@ -20,7 +20,7 @@ import (
"github.com/syncthing/syncthing/lib/events"
"github.com/syncthing/syncthing/lib/protocol"
"github.com/syncthing/syncthing/lib/svcutil"
"github.com/syncthing/syncthing/lib/ur"
"github.com/syncthing/syncthing/lib/ur/contract"
)
type indexHandler struct {
@@ -276,7 +276,7 @@ func (s *indexHandler) sendIndexTo(ctx context.Context) error {
// can't happen, once an error is returned the index sender exits
panic(fmt.Sprintf("bug: once failed it should stay failed (%v)", batchError))
}
l.Debugf("%v: Sending %d files (<%d bytes)", s, len(fs), batch.Size())
l.Debugf("%v: Sending %d files", s, len(fs))
lastSequence := fs[len(fs)-1].Sequence
var err error
@@ -470,7 +470,7 @@ func (s *indexHandler) logSequenceAnomaly(msg string, extra map[string]any) {
extraStrs[k] = fmt.Sprint(v)
}
s.evLogger.Log(events.Failure, ur.FailureData{
s.evLogger.Log(events.Failure, contract.FailureData{
Description: msg,
Extra: extraStrs,
})
+21 -34
View File
@@ -300,7 +300,7 @@ func (m *model) initFolders(cfg config.Configuration) error {
folderCfg.CreateRoot()
continue
}
err := m.newFolder(folderCfg, cfg.Options.CacheIgnoredFiles)
err := m.newFolder(folderCfg)
if err != nil {
return err
}
@@ -335,8 +335,8 @@ func (m *model) fatal(err error) {
}
// Need to hold lock on m.mut when calling this.
func (m *model) addAndStartFolderLocked(cfg config.FolderConfiguration, cacheIgnoredFiles bool) {
ignores := ignore.New(cfg.Filesystem(), ignore.WithCache(cacheIgnoredFiles))
func (m *model) addAndStartFolderLocked(cfg config.FolderConfiguration) {
ignores := ignore.New(cfg.Filesystem())
if cfg.Type != config.FolderTypeReceiveEncrypted {
if err := ignores.Load(".stignore"); err != nil && !fs.IsNotExist(err) {
slog.Error("Failed to load ignores", slogutil.Error(err))
@@ -371,7 +371,7 @@ func (m *model) addAndStartFolderLockedWithIgnores(cfg config.FolderConfiguratio
for _, available := range devs {
if _, ok := expected[available]; !ok {
l.Debugln("dropping", folder, "state for", available)
_ = m.sdb.DropAllFiles(folder, available)
_ = m.sdb.DropFolderDevice(folder, available)
}
}
@@ -509,7 +509,7 @@ func (m *model) cleanupFolderLocked(cfg config.FolderConfiguration) {
delete(m.folderEncryptionFailures, cfg.ID)
}
func (m *model) restartFolder(from, to config.FolderConfiguration, cacheIgnoredFiles bool) error {
func (m *model) restartFolder(from, to config.FolderConfiguration) error {
if to.ID == "" {
panic("bug: cannot restart empty folder ID")
}
@@ -539,7 +539,7 @@ func (m *model) restartFolder(from, to config.FolderConfiguration, cacheIgnoredF
m.cleanupFolderLocked(from)
if !to.Paused {
m.addAndStartFolderLocked(to, cacheIgnoredFiles)
m.addAndStartFolderLocked(to)
}
runner, _ := m.folderRunners.Get(to.ID)
@@ -560,11 +560,11 @@ func (m *model) restartFolder(from, to config.FolderConfiguration, cacheIgnoredF
return nil
}
func (m *model) newFolder(cfg config.FolderConfiguration, cacheIgnoredFiles bool) error {
func (m *model) newFolder(cfg config.FolderConfiguration) error {
m.mut.Lock()
defer m.mut.Unlock()
m.addAndStartFolderLocked(cfg, cacheIgnoredFiles)
m.addAndStartFolderLocked(cfg)
// Cluster configs might be received and processed before reaching this
// point, i.e. before the folder is started. If that's the case, start
@@ -1784,7 +1784,7 @@ func (m *model) handleAutoAccepts(deviceID protocol.DeviceID, folder protocol.Fo
// Attempt to create it to make sure it does, now.
fullPath := filepath.Join(defaultFolderCfg.Path, path)
if err := defaultPathFs.MkdirAll(path, 0o700); err != nil {
if err := defaultPathFs.MkdirAll(path, fs.ModePerm); err != nil {
slog.Error("Failed to create path for auto-accepted folder", folder.LogAttr(), slogutil.FilePath(fullPath), slogutil.Error(err))
continue
}
@@ -2079,7 +2079,7 @@ func (m *model) Request(conn protocol.Connection, req *protocol.Request) (out pr
return nil, protocol.ErrGeneric
}
if folderCfg.Type != config.FolderTypeReceiveEncrypted && len(req.Hash) > 0 && !scanner.Validate(res.data[:n], req.Hash) {
if folderCfg.Type != config.FolderTypeReceiveEncrypted && !scanner.Validate(res.data[:n], req.Hash) {
m.recheckFile(deviceID, req.Folder, req.Name, req.Offset, req.Hash)
l.Debugf("%v REQ(in) failed validating data: %s: %q / %q o=%d s=%d", m, deviceID.Short(), req.Folder, req.Name, req.Offset, req.Size)
return nil, protocol.ErrNoSuchFile
@@ -2548,13 +2548,6 @@ func (m *model) DelayScan(folder string, next time.Duration) {
func (m *model) numHashers(folder string) int {
m.mut.RLock()
folderCfg := m.folderCfgs[folder]
numFolders := max(1, len(m.folderCfgs))
// MaxFolderConcurrency already limits the number of scanned folders, so
// prefer it over the overall number of folders to avoid limiting performance
// further for no reason.
if concurrency := m.cfg.Options().MaxFolderConcurrency(); concurrency > 0 {
numFolders = min(numFolders, concurrency)
}
m.mut.RUnlock()
if folderCfg.Hashers > 0 {
@@ -2562,21 +2555,15 @@ func (m *model) numHashers(folder string) int {
return folderCfg.Hashers
}
numCpus := runtime.GOMAXPROCS(-1)
if build.IsWindows || build.IsDarwin || build.IsIOS || build.IsAndroid {
// Interactive operating systems; don't load the system too heavily by
// default.
numCpus = max(1, numCpus/4)
numCPUs := runtime.GOMAXPROCS(-1)
switch {
case build.IsWindows || build.IsIOS || build.IsAndroid:
// Use a quarter of the CPU cores on interactive or constrained OSes
return max(1, numCPUs/4)
default:
// Otherwise use up to half
return max(1, numCPUs/2)
}
// For other operating systems and architectures, lets try to get some
// work done... Divide the available CPU cores among the configured
// folders.
if perFolder := numCpus / numFolders; perFolder > 0 {
return perFolder
}
return 1
}
// generateClusterConfig returns a ClusterConfigMessage that is correct and the
@@ -2985,7 +2972,7 @@ func (m *model) CommitConfiguration(from, to config.Configuration) bool {
slog.Info("Paused folder", cfg.LogAttr())
} else {
slog.Info("Adding folder", cfg.LogAttr())
if err := m.newFolder(cfg, to.Options.CacheIgnoredFiles); err != nil {
if err := m.newFolder(cfg); err != nil {
m.fatal(err)
return true
}
@@ -3011,8 +2998,8 @@ func (m *model) CommitConfiguration(from, to config.Configuration) bool {
// This folder exists on both sides. Settings might have changed.
// Check if anything differs that requires a restart.
if !reflect.DeepEqual(fromCfg.RequiresRestartOnly(), toCfg.RequiresRestartOnly()) || from.Options.CacheIgnoredFiles != to.Options.CacheIgnoredFiles {
if err := m.restartFolder(fromCfg, toCfg, to.Options.CacheIgnoredFiles); err != nil {
if !reflect.DeepEqual(fromCfg.RequiresRestartOnly(), toCfg.RequiresRestartOnly()) {
if err := m.restartFolder(fromCfg, toCfg); err != nil {
m.fatal(err)
return true
}
+37 -21
View File
@@ -9,6 +9,7 @@ package model
import (
"bytes"
"context"
"crypto/sha256"
"encoding/json"
"errors"
"fmt"
@@ -93,8 +94,10 @@ func TestRequest(t *testing.T) {
m.ScanFolder("default")
foobarHash := sha256.Sum256([]byte("foobar"))
// Existing, shared file
res, err := m.Request(device1Conn, &protocol.Request{Folder: "default", Name: "foo", Size: 6})
res, err := m.Request(device1Conn, &protocol.Request{Folder: "default", Name: "foo", Size: 6, Hash: foobarHash[:]})
if err != nil {
t.Fatal(err)
}
@@ -104,35 +107,42 @@ func TestRequest(t *testing.T) {
}
// Existing, nonshared file
_, err = m.Request(device2Conn, &protocol.Request{Folder: "default", Name: "foo", Size: 6})
_, err = m.Request(device2Conn, &protocol.Request{Folder: "default", Name: "foo", Size: 6, Hash: foobarHash[:]})
if err == nil {
t.Error("Unexpected nil error on insecure file read")
}
// Nonexistent file
_, err = m.Request(device1Conn, &protocol.Request{Folder: "default", Name: "nonexistent", Size: 6})
_, err = m.Request(device1Conn, &protocol.Request{Folder: "default", Name: "nonexistent", Size: 6, Hash: foobarHash[:]})
if err == nil {
t.Error("Unexpected nil error on insecure file read")
}
// Shared folder, but disallowed file name
_, err = m.Request(device1Conn, &protocol.Request{Folder: "default", Name: "../walk.go", Size: 6})
_, err = m.Request(device1Conn, &protocol.Request{Folder: "default", Name: "../walk.go", Size: 6, Hash: foobarHash[:]})
if err == nil {
t.Error("Unexpected nil error on insecure file read")
}
// Negative size
_, err = m.Request(device1Conn, &protocol.Request{Folder: "default", Name: "foo", Size: -4})
_, err = m.Request(device1Conn, &protocol.Request{Folder: "default", Name: "foo", Size: -4, Hash: foobarHash[:]})
if err == nil {
t.Error("Unexpected nil error on insecure file read")
}
// Larger block than available
// Missing hash
_, err = m.Request(device1Conn, &protocol.Request{Folder: "default", Name: "foo", Size: 6})
if err == nil {
t.Error("Unexpected nil error on request without hash")
}
// Larger block than available, with a mismatched hash
_, err = m.Request(device1Conn, &protocol.Request{Folder: "default", Name: "foo", Size: 42, Hash: []byte("hash necessary but not checked")})
if err == nil {
t.Error("Unexpected nil error on read past end of file")
}
_, err = m.Request(device1Conn, &protocol.Request{Folder: "default", Name: "foo", Size: 42})
// Larger block than available, with the matching hash of the short read
_, err = m.Request(device1Conn, &protocol.Request{Folder: "default", Name: "foo", Size: 42, Hash: foobarHash[:]})
if err != nil {
t.Error("Unexpected error when large read should be permitted")
}
@@ -1593,7 +1603,7 @@ func TestIgnores(t *testing.T) {
ID: "fresh", Path: "XXX",
FilesystemType: config.FilesystemTypeFake,
}
ignores := ignore.New(fcfg.Filesystem(), ignore.WithCache(m.cfg.Options().CacheIgnoredFiles))
ignores := ignore.New(fcfg.Filesystem())
m.mut.Lock()
m.folderCfgs[fcfg.ID] = fcfg
m.folderIgnores[fcfg.ID] = ignores
@@ -1608,7 +1618,7 @@ func TestIgnores(t *testing.T) {
pausedDefaultFolderConfig := defaultFolderConfig
pausedDefaultFolderConfig.Paused = true
m.restartFolder(defaultFolderConfig, pausedDefaultFolderConfig, false)
m.restartFolder(defaultFolderConfig, pausedDefaultFolderConfig)
// Here folder initialization is not an issue as a paused folder isn't
// added to the model and thus there is no initial scan happening.
@@ -1777,10 +1787,12 @@ func TestGlobalDirectoryTree(t *testing.T) {
b := func(isfile bool, path ...string) protocol.FileInfo {
typ := protocol.FileInfoTypeDirectory
var blocks []protocol.BlockInfo
var size int64
if isfile {
typ = protocol.FileInfoTypeFile
blocks = []protocol.BlockInfo{{Offset: 0x0, Size: 0xa, Hash: []uint8{0x2f, 0x72, 0xcc, 0x11, 0xa6, 0xfc, 0xd0, 0x27, 0x1e, 0xce, 0xf8, 0xc6, 0x10, 0x56, 0xee, 0x1e, 0xb1, 0x24, 0x3b, 0xe3, 0x80, 0x5b, 0xf9, 0xa9, 0xdf, 0x98, 0xf9, 0x2f, 0x76, 0x36, 0xb0, 0x5c}}}
size = 0xa
}
seq++
return protocol.FileInfo{
@@ -1788,7 +1800,7 @@ func TestGlobalDirectoryTree(t *testing.T) {
Type: typ,
ModifiedS: 0x666,
Blocks: blocks,
Size: 0xa,
Size: size,
Sequence: seq,
}
}
@@ -1804,7 +1816,7 @@ func TestGlobalDirectoryTree(t *testing.T) {
return &TreeEntry{
Name: name,
ModTime: time.Unix(0x666, 0),
Size: 128,
Size: 0,
Type: protocol.FileInfoTypeDirectory.String(),
Children: entries,
}
@@ -2204,7 +2216,7 @@ func TestIndexesForUnknownDevicesDropped(t *testing.T) {
t.Error("expected two devices")
}
m.newFolder(defaultFolderConfig, false)
m.newFolder(defaultFolderConfig)
defer cleanupModel(m)
if devs, err := m.sdb.ListDevicesForFolder("default"); err != nil || len(devs) != 1 {
@@ -2979,15 +2991,16 @@ func TestRequestLimit(t *testing.T) {
defer cleanupModel(m)
m.ScanFolder("default")
emptyHash := sha256.Sum256(nil)
befReq := time.Now()
first, err := m.Request(conn, &protocol.Request{Folder: "default", Name: file, Size: 2000})
first, err := m.Request(conn, &protocol.Request{Folder: "default", Name: file, Size: 2000, Hash: emptyHash[:]})
if err != nil {
t.Fatalf("First request failed: %v", err)
}
reqDur := time.Since(befReq)
returned := make(chan struct{})
go func() {
second, err := m.Request(conn, &protocol.Request{Folder: "default", Name: file, Size: 2000})
second, err := m.Request(conn, &protocol.Request{Folder: "default", Name: file, Size: 2000, Hash: emptyHash[:]})
if err != nil {
t.Errorf("Second request failed: %v", err)
}
@@ -3257,17 +3270,20 @@ func TestRenameSequenceOrder(t *testing.T) {
t.Errorf("Unexpected count: %d != %d", count, numFiles)
}
// Modify all the files, other than the ones we expect to rename
// Modify all the files other than the rename sources, whose content we
// keep intact so the renamed copies still match by block hash.
for i := 0; i < numFiles; i++ {
if i == 3 || i == 17 || i == 16 || i == 4 {
if i == 3 || i == 16 {
continue
}
v := fmt.Sprintf("%d", i)
writeFile(t, ffs, v, []byte(v+"-new"))
}
// Rename
must(t, ffs.Rename("3", "17"))
must(t, ffs.Rename("16", "4"))
// Rename to previously unseen names. Renaming onto an existing name is
// treated as an in-place update rather than a rename, since rename
// detection only runs for files that are new on disk.
must(t, ffs.Rename("3", "20"))
must(t, ffs.Rename("16", "21"))
// Scan
m.ScanFolders()
@@ -3278,10 +3294,10 @@ func TestRenameSequenceOrder(t *testing.T) {
it, errFn := m.LocalFilesSequenced("default", protocol.LocalDeviceID, 0)
for i := range it {
t.Log(i)
if i.FileName() == "17" {
if i.FileName() == "20" {
firstExpectedSequence = i.SequenceNo() + 1
}
if i.FileName() == "4" {
if i.FileName() == "21" {
secondExpectedSequence = i.SequenceNo() + 1
}
if i.FileName() == "3" {
+1 -1
View File
@@ -250,7 +250,7 @@ func (*alwaysChanged) Changed() bool {
func folderIgnoresAlwaysReload(t testing.TB, m *testModel, fcfg config.FolderConfiguration) {
t.Helper()
m.removeFolder(fcfg)
ignores := ignore.New(fcfg.Filesystem(), ignore.WithCache(true), ignore.WithChangeDetector(newAlwaysChanged()))
ignores := ignore.New(fcfg.Filesystem(), ignore.WithChangeDetector(newAlwaysChanged()))
m.mut.Lock()
m.addAndStartFolderLockedWithIgnores(fcfg, ignores)
m.mut.Unlock()
+23 -5
View File
@@ -8,8 +8,10 @@ package osutil_test
import (
"path/filepath"
"slices"
"testing"
"github.com/syncthing/syncthing/lib/build"
"github.com/syncthing/syncthing/lib/fs"
"github.com/syncthing/syncthing/lib/osutil"
"github.com/syncthing/syncthing/lib/rand"
@@ -59,11 +61,16 @@ func TestTraversesSymlink(t *testing.T) {
}
}
func TestIssue4875(t *testing.T) {
testFsPath := rand.String(32)
testFs := fs.NewFilesystem(fs.FilesystemTypeFake, testFsPath)
func TestSymlinkedFSRootTraversal(t *testing.T) {
// Normal FS operations should work inside a folder root that is a symlink
if build.IsWindows {
t.Skip("symlinks unavailable")
}
testFsPath := t.TempDir()
testFs := fs.NewFilesystem(fs.FilesystemTypeBasic, testFsPath)
testFs.MkdirAll(filepath.Join("a", "b", "c"), 0o755)
if err := testFs.CreateSymlink(filepath.Join("a", "b"), filepath.Join("a", "l")); err != nil {
if err := testFs.CreateSymlink("b", filepath.Join("a", "l")); err != nil {
t.Fatal(err)
}
@@ -76,10 +83,21 @@ func TestIssue4875(t *testing.T) {
t.Fatal("error in setup, a/l/c should be a directory")
}
testFs = fs.NewFilesystem(fs.FilesystemTypeFake, filepath.Join(testFsPath, "a/l"))
testFs = fs.NewFilesystem(fs.FilesystemTypeBasic, filepath.Join(testFsPath, "a/l"))
if err := osutil.TraversesSymlink(testFs, "."); err != nil {
t.Error(`TraversesSymlink on filesystem with symlink at root returned error for ".":`, err)
}
// We should be able to create and list files within the symlinked root
if _, err := testFs.Create("test"); err != nil {
t.Fatal(err)
}
if names, err := testFs.DirNames("."); err != nil {
t.Fatal(err)
} else if !slices.Contains(names, "test") {
t.Log(names)
t.Error("missing test file in listing")
}
}
var traversesSymlinkResult error
+4 -11
View File
@@ -146,6 +146,8 @@ type FileInfo struct {
Deleted bool
NoPermissions bool
New bool // set by the scanner when the file newly appeared on disk; no db / wire representation
truncated bool // was created from a truncated file info without blocks
}
@@ -233,7 +235,8 @@ func (f *FileInfo) LogAttr() slog.Attr {
case FileInfoTypeFile:
kind = "file"
if !f.Deleted {
attrs = append(attrs,
attrs = append(
attrs,
slog.Any("modified", f.ModTime()),
slog.String("permissions", fmt.Sprintf("0%03o", f.Permissions)),
slog.Int64("size", f.Size),
@@ -389,16 +392,6 @@ func (f FileInfo) HasPermissionBits() bool {
return !f.NoPermissions
}
func (f FileInfo) FileSize() int64 {
if f.Deleted {
return 0
}
if f.IsDirectory() || f.IsSymlink() {
return SyntheticDirectorySize
}
return f.Size
}
func (f FileInfo) BlockSize() int {
if f.RawBlockSize < MinBlockSize {
return MinBlockSize
+13 -19
View File
@@ -93,31 +93,25 @@ func (e encryptedModel) Request(req *Request) (RequestResponse, error) {
}
realSize := req.Size - blockOverhead
realOffset := req.Offset - int64(req.BlockNo*blockOverhead)
if realOffset < 0 {
panic("bug: realOffset underflow")
}
if req.Size < minPaddedSize {
return nil, errors.New("short request")
}
// Attempt to decrypt the block hash; it may be nil depending on what
// type of device the request comes from. Trusted devices with
// encryption enabled know the hash but don't bother to encrypt & send
// it to us. Untrusted devices have the hash from the encrypted index
// data and do send it. The model knows to only verify the hash if it
// actually gets one.
var realHash []byte
// Decrypt the block hash.
fileKey := e.keyGen.FileKey(realName, folderKey)
if len(req.Hash) > 0 {
var additional [8]byte
binary.BigEndian.PutUint64(additional[:], uint64(realOffset))
realHash, err = decryptDeterministic(req.Hash, fileKey, additional[:])
if err != nil {
// "Legacy", no offset additional data?
realHash, err = decryptDeterministic(req.Hash, fileKey, nil)
}
if err != nil {
return nil, fmt.Errorf("decrypting block hash: %w", err)
}
var additional [8]byte
binary.BigEndian.PutUint64(additional[:], uint64(realOffset))
realHash, err := decryptDeterministic(req.Hash, fileKey, additional[:])
if err != nil {
// "Legacy", no offset additional data?
realHash, err = decryptDeterministic(req.Hash, fileKey, nil)
}
if err != nil {
return nil, fmt.Errorf("decrypting block hash: %w", err)
}
// Perform that request and grab the data.
+30 -14
View File
@@ -56,7 +56,8 @@ const (
// DesiredPerFileBlocks is the number of blocks we aim for per file
DesiredPerFileBlocks = 2000
SyntheticDirectorySize = 128
// We used to send this size for directories and still accept that for compat.
deprecatedSyntheticDirectorySize = 128
// don't bother compressing messages smaller than this many bytes
compressionThreshold = 128
@@ -68,15 +69,16 @@ const (
)
var (
ErrClosed = errors.New("connection closed")
ErrTimeout = errors.New("read timeout")
errNotCompressible = errors.New("not compressible")
errUnknownMessage = errors.New("unknown message")
errInvalidFilename = errors.New("filename is invalid")
errUncleanFilename = errors.New("filename not in canonical format")
errDeletedHasBlocks = errors.New("deleted file with non-empty block list")
errDirectoryHasBlocks = errors.New("directory with non-empty block list")
errFileHasNoBlocks = errors.New("file with empty block list")
ErrClosed = errors.New("connection closed")
ErrTimeout = errors.New("read timeout")
errNotCompressible = errors.New("not compressible")
errUnknownMessage = errors.New("unknown message")
errInvalidFilename = errors.New("filename is invalid")
errUncleanFilename = errors.New("filename not in canonical format")
errDeletedHasBlocks = errors.New("deleted file with non-empty block list")
errNonFileHasBlocks = errors.New("non-file type with non-empty block list")
errNonFileHasSize = errors.New("non-file type with nonzero size")
errFileHasNoBlocks = errors.New("file with empty block list")
)
type Model interface {
@@ -483,12 +485,18 @@ func (c *rawConnection) dispatcherLoop() (err error) {
if err := checkFilename(msg.Name); err != nil {
return newProtocolError(err, msgContext)
}
if msg.Size <= 0 {
if msg.Size < 0 {
return newProtocolError(fmt.Errorf("request size %d too small", msg.Size), msgContext)
}
if msg.Size > MaxRequestSize {
return newProtocolError(fmt.Errorf("request size %d exceeds maximum allowed", msg.Size), msgContext)
}
if len(msg.Hash) == 0 {
// Syncthing versions older than v1.28.1 omit the hash in
// encrypted requests from trusted devices (a rare config)
// and will run into this.
return newProtocolError(errors.New("request missing block hash"), msgContext)
}
go c.handleRequest(requestFromWire(msg))
case *bep.Response:
@@ -629,9 +637,17 @@ func checkFileInfoConsistency(f FileInfo) error {
// Deleted files should have no blocks
return errDeletedHasBlocks
case f.Type == FileInfoTypeDirectory && len(f.Blocks) != 0:
// Directories should have no blocks
return errDirectoryHasBlocks
case f.Type != FileInfoTypeFile && len(f.Blocks) != 0:
// Only files should have blocks
return errNonFileHasBlocks
case f.IsDirectory() && f.Size != 0 && f.Size != deprecatedSyntheticDirectorySize:
// Directories should be size zero or the synthetic directory size
return errNonFileHasSize
case f.IsSymlink() && f.Size != 0:
// Symlinks should be size zero
return errNonFileHasSize
case !f.Deleted && !f.IsInvalid() && f.Type == FileInfoTypeFile && len(f.Blocks) == 0:
// Non-deleted, non-invalid files should have at least one block
+88 -1
View File
@@ -451,6 +451,61 @@ func TestCheckConsistency(t *testing.T) {
},
ok: false,
},
{
// directory with zero size
fi: FileInfo{
Name: "foo",
Type: FileInfoTypeDirectory,
},
ok: true,
},
{
// directory with synthetic size
fi: FileInfo{
Name: "foo",
Type: FileInfoTypeDirectory,
Size: deprecatedSyntheticDirectorySize,
},
ok: true,
},
{
// directory with arbitrary size
fi: FileInfo{
Name: "foo",
Type: FileInfoTypeDirectory,
Size: 42,
},
ok: false,
},
{
// symlink with zero size
fi: FileInfo{
Name: "foo",
Type: FileInfoTypeSymlink,
SymlinkTarget: []byte("bar"),
},
ok: true,
},
{
// symlink with synthetic directory size (not permitted)
fi: FileInfo{
Name: "foo",
Type: FileInfoTypeSymlink,
SymlinkTarget: []byte("bar"),
Size: deprecatedSyntheticDirectorySize,
},
ok: false,
},
{
// symlink with arbitrary size
fi: FileInfo{
Name: "foo",
Type: FileInfoTypeSymlink,
SymlinkTarget: []byte("bar"),
Size: 42,
},
ok: false,
},
}
for _, tc := range cases {
@@ -544,7 +599,7 @@ func TestDispatcherToCloseDeadlock(t *testing.T) {
}
func TestRequestMaxSize(t *testing.T) {
invalidSize := []int{-65536, 0, MaxRequestSize + 1}
invalidSize := []int{-65536, -1, MaxRequestSize + 1}
for _, s := range invalidSize {
t.Run(fmt.Sprintf("invalid/%d", s), func(t *testing.T) {
m := newTestModel()
@@ -560,6 +615,7 @@ func TestRequestMaxSize(t *testing.T) {
Id: 1,
Name: "valid",
Size: MaxRequestSize,
Hash: []byte{42},
}
res := <-c.outbox
@@ -573,6 +629,7 @@ func TestRequestMaxSize(t *testing.T) {
Id: 2,
Name: "invalid",
Size: int32(s),
Hash: []byte{42},
}
select {
@@ -592,6 +649,35 @@ func TestRequestMaxSize(t *testing.T) {
}
}
func TestRequestZeroSize(t *testing.T) {
// A zero-sized request should be accepted, since current versions of
// Syncthing send these. See https://github.com/syncthing/syncthing/issues/10709.
m := newTestModel()
rw := testutil.NewBlockingRW()
c := getRawConnection(NewConnection(c0ID, rw, &testutil.NoopRW{}, testutil.NoopCloser{}, m, new(mockedConnectionInfo), CompressionAlways, testKeyGen))
c.Start()
defer closeAndWait(c, rw)
c.inbox <- &bep.ClusterConfig{}
c.inbox <- &bep.Request{
Id: 1,
Name: "valid",
Size: 0,
Hash: []byte{42},
}
select {
case res := <-c.outbox:
if msg, ok := res.msg.(*bep.Response); !ok || msg.Id != 1 {
t.Errorf("bad response %#v", msg)
}
case <-c.dispatcherLoopStopped:
t.Fatal("dispatcher loop terminated, expected zero-sized request to be accepted")
case <-time.After(time.Second):
t.Fatal("timed out waiting for response")
}
}
func TestRequestInvalidFilename(t *testing.T) {
m := newTestModel()
rw := testutil.NewBlockingRW()
@@ -604,6 +690,7 @@ func TestRequestInvalidFilename(t *testing.T) {
Id: 1,
Name: "../escape",
Size: 1024,
Hash: []byte{42},
}
select {
+3 -16
View File
@@ -120,25 +120,12 @@ func Blocks(ctx context.Context, r io.Reader, blocksize int, sizehint int64, cou
return blocks, nil
}
// Validate validates the hash, if len(hash)>0.
// Validate validates the hash.
func Validate(buf, hash []byte) bool {
if len(hash) > 0 {
hbuf := sha256.Sum256(buf)
return bytes.Equal(hbuf[:], hash)
}
return true
hbuf := sha256.Sum256(buf)
return bytes.Equal(hbuf[:], hash)
}
type noopHash struct{}
func (noopHash) Sum32() uint32 { return 0 }
func (noopHash) BlockSize() int { return 0 }
func (noopHash) Size() int { return 0 }
func (noopHash) Reset() {}
func (noopHash) Sum([]byte) []byte { return nil }
func (noopHash) Write([]byte) (int, error) { return 0, nil }
type noopCounter struct{}
func (*noopCounter) Update(_ int64) {}
+22 -3
View File
@@ -267,9 +267,22 @@ func (w *walker) scan(ctx context.Context, toHashChan chan<- protocol.FileInfo,
// isWarnableError returns true if err is a kind of error we should warn
// about receiving from the folder walk.
func isWarnableError(err error) bool {
return err != nil &&
!errors.Is(err, fs.SkipDir) && // intentional skip
!errors.Is(err, context.Canceled) // folder restarting
switch {
case err == nil:
return false
case errors.Is(err, fs.SkipDir):
// intentional skip
return false
case errors.Is(err, context.Canceled):
// folder restarting
return false
case fs.IsNotExist(err):
// sudden does-not-exist is fine for deleted files, deleted folder
// root triggers health check error instead
return false
default:
return true
}
}
func (w *walker) walkAndHashFiles(ctx context.Context, toHashChan chan<- protocol.FileInfo, finishedChan chan<- ScanResult) fs.WalkFunc {
@@ -458,6 +471,8 @@ func (w *walker) walkRegular(ctx context.Context, relPath string, info fs.FileIn
f.RawBlockSize = int32(blockSize)
l.Debugln(w, "checking:", f)
f.New = !hasCurFile
if hasCurFile {
if curFile.IsEquivalentOptional(f, protocol.FileInfoComparison{
ModTimeWindow: w.ModTimeWindow,
@@ -505,6 +520,8 @@ func (w *walker) walkDir(ctx context.Context, relPath string, info fs.FileInfo,
f.NoPermissions = w.IgnorePerms
l.Debugln(w, "checking:", f)
f.New = !hasCurFile
if hasCurFile {
if curFile.IsEquivalentOptional(f, protocol.FileInfoComparison{
ModTimeWindow: w.ModTimeWindow,
@@ -557,6 +574,8 @@ func (w *walker) walkSymlink(ctx context.Context, relPath string, info fs.FileIn
f = w.updateFileInfo(f, curFile)
l.Debugln(w, "checking:", f)
f.New = !hasCurFile
if hasCurFile {
if curFile.IsEquivalentOptional(f, protocol.FileInfoComparison{
ModTimeWindow: w.ModTimeWindow,
+6 -6
View File
@@ -41,9 +41,9 @@ type testfileList []testfile
var testdata = testfileList{
{"afile", 4, "b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c"},
{"dir1", 128, ""},
{"dir1", 0, ""},
{filepath.Join("dir1", "dfile"), 5, "49ae93732fcf8d63fe1cce759664982dbd5b23161f007dba8561862adc96d063"},
{"dir2", 128, ""},
{"dir2", 0, ""},
{filepath.Join("dir2", "cfile"), 4, "bf07a7fbb825fc0aae7bf4a1177b2b31fcf8a3feeaf7092761e18c859ee52a9c"},
{"excludes", 37, "df90b52f0c55dba7a7a940affe482571563b1ac57bd5be4d8a0291e7de928e06"},
{"further-excludes", 5, "7eb0a548094fa6295f7fd9200d69973e5f5ec5c04f2a86d998080ac43ecf89f1"},
@@ -601,7 +601,7 @@ func (l fileList) testfiles() testfileList {
if len(f.Blocks) > 1 {
panic("simple test case stuff only supports a single block per file")
}
testfiles[i] = testfile{name: f.Name, length: f.FileSize()}
testfiles[i] = testfile{name: f.Name, length: f.Size}
if len(f.Blocks) == 1 {
testfiles[i].hash = fmt.Sprintf("%x", f.Blocks[0].Hash)
}
@@ -749,7 +749,7 @@ func TestRecurseInclude(t *testing.T) {
*
`
testFs := newTestFs()
ignores := ignore.New(testFs, ignore.WithCache(true))
ignores := ignore.New(testFs)
if err := ignores.Parse(bytes.NewBufferString(stignore), ".stignore"); err != nil {
t.Fatal(err)
}
@@ -859,7 +859,7 @@ func TestSkipIgnoredDirs(t *testing.T) {
w := &walker{}
pats := ignore.New(fss, ignore.WithCache(true))
pats := ignore.New(fss)
stignore := `
/foo/ign*
@@ -892,7 +892,7 @@ func TestIncludedSubdir(t *testing.T) {
t.Fatal(err)
}
pats := ignore.New(fss, ignore.WithCache(true))
pats := ignore.New(fss)
stignore := `
!/foo/bar
+2 -3
View File
@@ -554,7 +554,7 @@ func replaceRawPath(u *url.URL, rp string) {
q = fs[1]
}
if p[0] == '/' {
if p != "" && p[0] == '/' {
u.Path = p
} else {
u.Path += p
@@ -607,6 +607,7 @@ func soapRequestWithIP(ctx context.Context, url, service, function, message stri
l.Debugln("SOAP do:", err)
return resp, err
}
defer r.Body.Close()
resp, err = io.ReadAll(r.Body)
if err != nil {
@@ -616,8 +617,6 @@ func soapRequestWithIP(ctx context.Context, url, service, function, message stri
l.Debugf("SOAP Response: %s\n\n%s\n\n", r.Status, resp)
r.Body.Close()
if r.StatusCode >= 400 {
return resp, errors.New(function + ": " + r.Status)
}
+13
View File
@@ -79,3 +79,16 @@ func TestControlURLParsing(t *testing.T) {
t.Error("URL normalization of", subject, "failed; expected", expected, "got", u.String())
}
}
func TestControlURLParsingQueryOnly(t *testing.T) {
rootURL := "http://192.168.243.1:80/igd.xml"
u, _ := url.Parse(rootURL)
subject := "?control=WANCommonIFC1"
expected := "http://192.168.243.1:80/igd.xml?control=WANCommonIFC1"
replaceRawPath(u, subject)
if u.String() != expected {
t.Error("URL normalization of", subject, "failed; expected", expected, "got", u.String())
}
}
+13 -1
View File
@@ -91,7 +91,6 @@ type Report struct {
NATType string `json:"natType,omitempty" metric:"nat_detection,gaugeVec:type" since:"3"`
AlwaysLocalNets bool `json:"alwaysLocalNets,omitempty" metric:"feature_count{feature=AlwaysLocalNets},gauge" since:"3"`
CacheIgnoredFiles bool `json:"cacheIgnoredFiles,omitempty" metric:"feature_count{feature=CacheIgnoredFiles},gauge" since:"3"`
OverwriteRemoteDeviceNames bool `json:"overwriteRemoteDeviceNames,omitempty" metric:"feature_count{feature=OverwriteRemoteDeviceNames},gauge" since:"3"`
ProgressEmitterEnabled bool `json:"progressEmitterEnabled,omitempty" metric:"feature_count{feature=ProgressEmitterEnabled},gauge" since:"3"`
CustomDefaultFolderPath bool `json:"customDefaultFolderPath,omitempty" metric:"feature_count{feature=CustomDefaultFolderPath},gauge" since:"3"`
@@ -282,3 +281,16 @@ func clear(v interface{}, since int) error {
}
return nil
}
type FailureReport struct {
FailureData
Count int
Version string
}
type FailureData struct {
Description string
Goroutines string
Extra map[string]string
}
+14 -26
View File
@@ -23,6 +23,7 @@ import (
"github.com/syncthing/syncthing/lib/events"
"github.com/syncthing/syncthing/lib/svcutil"
"github.com/syncthing/syncthing/lib/tlsutil"
"github.com/syncthing/syncthing/lib/ur/contract"
"github.com/thejerf/suture/v4"
)
@@ -39,23 +40,10 @@ var (
invalidEventDataType = "failure event data is not a string"
)
type FailureReport struct {
FailureData
Count int
Version string
}
type FailureData struct {
Description string
Goroutines string
Extra map[string]string
}
func FailureDataWithGoroutines(description string) FailureData {
func FailureDataWithGoroutines(description string) contract.FailureData {
var buf strings.Builder
pprof.Lookup("goroutine").WriteTo(&buf, 1)
return FailureData{
return contract.FailureData{
Description: description,
Goroutines: buf.String(),
Extra: make(map[string]string),
@@ -86,7 +74,7 @@ type failureHandler struct {
type failureStat struct {
first, last time.Time
count int
data FailureData
data contract.FailureData
}
func (h *failureHandler) Serve(ctx context.Context) error {
@@ -105,24 +93,24 @@ func (h *failureHandler) Serve(ctx context.Context) error {
if !ok {
// Just to be safe - shouldn't ever happen, as
// evChan is set to nil when unsubscribing.
h.addReport(FailureData{Description: evChanClosed}, time.Now())
h.addReport(contract.FailureData{Description: evChanClosed}, time.Now())
evChan = nil
continue
}
var data FailureData
var data contract.FailureData
switch d := e.Data.(type) {
case string:
data.Description = d
case FailureData:
case contract.FailureData:
data = d
default:
// Same here, shouldn't ever happen.
h.addReport(FailureData{Description: invalidEventDataType}, time.Now())
h.addReport(contract.FailureData{Description: invalidEventDataType}, time.Now())
continue
}
h.addReport(data, e.Time)
case <-timer.C:
reports := make([]FailureReport, 0, len(h.buf))
reports := make([]contract.FailureReport, 0, len(h.buf))
now := time.Now()
for descr, stat := range h.buf {
if now.Sub(stat.last) > minDelay || now.Sub(stat.first) > maxDelay {
@@ -152,7 +140,7 @@ func (h *failureHandler) Serve(ctx context.Context) error {
if sub != nil {
sub.Unsubscribe()
if len(h.buf) > 0 {
reports := make([]FailureReport, 0, len(h.buf))
reports := make([]contract.FailureReport, 0, len(h.buf))
for _, stat := range h.buf {
reports = append(reports, newFailureReport(stat))
}
@@ -179,7 +167,7 @@ func (h *failureHandler) applyOpts(opts config.OptionsConfiguration, sub events.
return url, nil, nil
}
func (h *failureHandler) addReport(data FailureData, evTime time.Time) {
func (h *failureHandler) addReport(data contract.FailureData, evTime time.Time) {
if stat, ok := h.buf[data.Description]; ok {
stat.last = evTime
stat.count++
@@ -204,7 +192,7 @@ func (*failureHandler) String() string {
return "FailureHandler"
}
func sendFailureReports(ctx context.Context, reports []FailureReport, url string) {
func sendFailureReports(ctx context.Context, reports []contract.FailureReport, url string) {
var b bytes.Buffer
if err := json.NewEncoder(&b).Encode(reports); err != nil {
panic(err)
@@ -235,8 +223,8 @@ func sendFailureReports(ctx context.Context, reports []FailureReport, url string
resp.Body.Close()
}
func newFailureReport(stat *failureStat) FailureReport {
return FailureReport{
func newFailureReport(stat *failureStat) contract.FailureReport {
return contract.FailureReport{
FailureData: stat.data,
Count: stat.count,
Version: build.LongVersion,
-1
View File
@@ -217,7 +217,6 @@ func (s *Service) reportData(ctx context.Context, urVersion int, preview bool) (
report.Uptime = s.UptimeS()
report.NATType = s.connectionsService.NATType()
report.AlwaysLocalNets = len(opts.AlwaysLocalNets) > 0
report.CacheIgnoredFiles = opts.CacheIgnoredFiles
report.OverwriteRemoteDeviceNames = opts.OverwriteRemoteDevNames
report.ProgressEmitterEnabled = opts.ProgressUpdateIntervalS > -1
report.CustomDefaultFolderPath = defaultFolder.Path != "~"
+58 -27
View File
@@ -27,6 +27,8 @@ func init() {
factories["external"] = newExternal
}
const unixSpecialChars = "`" + `"'<>;!#$&*? `
type external struct {
command string
filesystem fs.Filesystem
@@ -68,36 +70,11 @@ func (v external) Archive(filePath string) error {
return errors.New("command is empty, please enter a valid command")
}
words, err := shellquote.Split(v.command)
cmd, err := v.prepareCommand(filePath)
if err != nil {
return fmt.Errorf("command is invalid: %w", err)
return err
}
context := map[string]string{
"%FOLDER_FILESYSTEM%": string(v.filesystem.Type()),
"%FOLDER_PATH%": v.filesystem.URI(),
"%FILE_PATH%": filePath,
}
for i, word := range words {
for key, val := range context {
word = strings.ReplaceAll(word, key, val)
}
words[i] = word
}
cmd := exec.Command(words[0], words[1:]...)
env := os.Environ()
// filter STGUIAUTH and STGUIAPIKEY from environment variables
var filteredEnv []string
for _, x := range env {
if !strings.HasPrefix(x, "STGUIAUTH=") && !strings.HasPrefix(x, "STGUIAPIKEY=") {
filteredEnv = append(filteredEnv, x)
}
}
cmd.Env = filteredEnv
combinedOutput, err := cmd.CombinedOutput()
l.Debugln("external command output:", string(combinedOutput))
if err != nil {
@@ -126,3 +103,57 @@ func (external) Restore(_ string, _ time.Time) error {
func (external) Clean(_ context.Context) error {
return nil
}
// prepareCommand returns the command with environment for the given file
// path.
func (v external) prepareCommand(filePath string) (*exec.Cmd, error) {
words, err := shellquote.Split(v.command)
if err != nil {
return nil, fmt.Errorf("command is invalid: %w", err)
}
if len(words) == 0 {
return nil, errors.New("command is empty")
}
context := map[string]string{
"%FOLDER_FILESYSTEM%": string(v.filesystem.Type()),
"%FOLDER_PATH%": v.filesystem.URI(),
"%FILE_PATH%": filePath,
}
for i, word := range words {
unsafe := strings.ContainsAny(word, unixSpecialChars)
for key, val := range context {
// If the parameter contains both an unsafe character and a
// template placeholder, we consider it unsafe and reject it.
// Note that the shell splitting will have already removed outer
// quotes, so that a command like `foo "%FILE_PATH%"` is fine
// here, despite the double quote being one of our unsafe
// characters.
if unsafe && strings.Contains(word, key) {
return nil, errors.New("unsafe external versioning command; see https://docs.syncthing.net/users/versioning.html#external-file-versioning")
}
word = strings.ReplaceAll(word, key, val)
}
words[i] = word
}
// filter STGUIAUTH and STGUIAPIKEY from environment variables, and add
// our folder info.
env := os.Environ()
var filteredEnv []string
for _, x := range env {
if !strings.HasPrefix(x, "STGUIAUTH=") && !strings.HasPrefix(x, "STGUIAPIKEY=") {
filteredEnv = append(filteredEnv, x)
}
}
for k, v := range context {
k = strings.Trim(k, "%")
filteredEnv = append(filteredEnv, fmt.Sprintf("%s=%s", k, v))
}
cmd := exec.Command(words[0], words[1:]...) //nolint:gosec // execution with user tainted data, by design
cmd.Env = filteredEnv
return cmd, nil
}
+47
View File
@@ -76,6 +76,53 @@ func TestExternal(t *testing.T) {
}
}
func TestExternalCommandSplit(t *testing.T) {
e := external{
filesystem: fs.NewFilesystem(fs.FilesystemTypeFake, "TestExternalCommandSplit"),
}
cases := []struct {
cmd string
safe bool
}{
{`echo %FOLDER_PATH% %FILE_PATH%`, true},
{`echo "%FOLDER_PATH% %FILE_PATH%"`, false},
{`echo %FOLDER_PATH%/%FILE_PATH%`, true},
{`echo "%FOLDER_PATH%/%FILE_PATH%"`, true},
{`echo '%FOLDER_PATH%/%FILE_PATH%'`, true},
{`echo "'%FOLDER_PATH%/%FILE_PATH%'"`, false},
{`sh -c "echo '%FOLDER_PATH%/%FILE_PATH%'"`, false},
{`sh -c "echo %FOLDER_PATH%/%FILE_PATH%"`, false},
}
for _, tc := range cases {
e.command = tc.cmd
res, err := e.prepareCommand("evil file name")
if tc.safe && err != nil {
t.Fatal(err)
}
if !tc.safe && err == nil {
t.Logf("%q", res.Path)
t.Logf("%q", res.Args)
t.Errorf("should be unsafe: %q", tc.cmd)
}
}
}
func TestExternalWhitespaceCommand(t *testing.T) {
// A whitespace-only command splits into zero words; prepareCommand must
// return an error rather than panicking on words[0].
e := external{
filesystem: fs.NewFilesystem(fs.FilesystemTypeFake, "TestExternalWhitespaceCommand"),
}
for _, cmd := range []string{" ", "\t", "\n", " \t \n"} {
e.command = cmd
if _, err := e.prepareCommand("file"); err == nil {
t.Errorf("expected error for command %q, got nil", cmd)
}
}
}
func prepForRemoval(t *testing.T, file string) {
if err := os.RemoveAll("testdata"); err != nil {
t.Fatal(err)

Some files were not shown because too many files have changed in this diff Show More