Compare commits
15
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f1d631d66e | ||
|
|
ee275fee65 | ||
|
|
6606e6b4d3 | ||
|
|
f6428af4c8 | ||
|
|
a5cbeeafea | ||
|
|
5dbf809a4c | ||
|
|
e16b8013a6 | ||
|
|
6df85dc95c | ||
|
|
f93306c819 | ||
|
|
9cdff04877 | ||
|
|
9bb5f9b4dd | ||
|
|
8961f642c6 | ||
|
|
d91f8849a2 | ||
|
|
6be1ff8480 | ||
|
|
3ec73403c1 |
@@ -6,6 +6,3 @@ vendor/** -text=auto
|
||||
|
||||
# Diffs on these files are meaningless
|
||||
*.svg -diff
|
||||
|
||||
# Patch files intentionally contain diff context whitespace
|
||||
patches/*.patch -whitespace
|
||||
|
||||
@@ -1,135 +0,0 @@
|
||||
name: custom release
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
releases: write
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- ".gitea/workflows/custom-release.yml"
|
||||
- "patches/**"
|
||||
- "scripts/update-custom-release.sh"
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
upstream_tag:
|
||||
description: "Optional upstream Syncthing tag, for example v2.1.0"
|
||||
required: false
|
||||
suffix:
|
||||
description: "Optional custom release suffix, for example stignore.7"
|
||||
required: false
|
||||
schedule:
|
||||
- cron: "17 04 * * *"
|
||||
|
||||
jobs:
|
||||
build-custom-release:
|
||||
runs-on: ffmini_macos_arm64
|
||||
steps:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
cache: false
|
||||
|
||||
- name: Configure Git author
|
||||
run: |
|
||||
git config user.name "Gitea Actions"
|
||||
git config user.email "actions@git.felixfoertsch.de"
|
||||
|
||||
- name: Set up tea
|
||||
run: |
|
||||
go install code.gitea.io/tea@v0.14.1
|
||||
echo "$(go env GOPATH)/bin" >> "$GITHUB_PATH"
|
||||
"$(go env GOPATH)/bin/tea" logins delete actions >/dev/null 2>&1 || true
|
||||
"$(go env GOPATH)/bin/tea" logins add --name actions --url https://git.felixfoertsch.de --token "$GITEA_TOKEN" --no-version-check
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||
|
||||
- name: Import Developer ID certificate
|
||||
run: |
|
||||
set -euo pipefail
|
||||
keychain_dir="$HOME/Library/Keychains"
|
||||
mkdir -p "$keychain_dir"
|
||||
keychain_path="$keychain_dir/syncthing-release-signing-${GITHUB_RUN_ID:-$$}.keychain-db"
|
||||
keychain_password="$(openssl rand -hex 24)"
|
||||
certificate_path="$RUNNER_TEMP/developer-id-application.p12"
|
||||
previous_default_keychain="$(security default-keychain -d user 2>/dev/null | sed 's/[ "]//g' || true)"
|
||||
|
||||
echo "CUSTOM_RELEASE_KEYCHAIN_PATH=$keychain_path" >> "$GITHUB_ENV"
|
||||
echo "CUSTOM_RELEASE_KEYCHAIN_PASSWORD=$keychain_password" >> "$GITHUB_ENV"
|
||||
echo "CUSTOM_RELEASE_PREVIOUS_DEFAULT_KEYCHAIN=$previous_default_keychain" >> "$GITHUB_ENV"
|
||||
|
||||
if [ -z "$DEVELOPER_ID_APPLICATION_P12_BASE64" ]; then
|
||||
echo "DEVELOPER_ID_APPLICATION_P12_BASE64 secret is required" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf '%s' "$DEVELOPER_ID_APPLICATION_P12_BASE64" | base64 -D > "$certificate_path"
|
||||
rm -f "$keychain_path"
|
||||
security create-keychain -p "$keychain_password" "$keychain_path"
|
||||
security set-keychain-settings -lut 21600 "$keychain_path"
|
||||
security unlock-keychain -p "$keychain_password" "$keychain_path"
|
||||
security import "$certificate_path" -k "$keychain_path" -P "$DEVELOPER_ID_APPLICATION_P12_PASSWORD" -A -T /usr/bin/codesign -T /usr/bin/security
|
||||
existing_keychains=()
|
||||
while IFS= read -r existing_keychain; do
|
||||
existing_keychain="$(printf '%s' "$existing_keychain" | sed 's/[ "]//g')"
|
||||
if [ -n "$existing_keychain" ] && [ -e "$existing_keychain" ] && [[ "$existing_keychain" != *"/syncthing-release-signing-"*".keychain-db" ]]; then
|
||||
existing_keychains+=("$existing_keychain")
|
||||
fi
|
||||
done < <(security list-keychains)
|
||||
security list-keychains -s "$keychain_path" "${existing_keychains[@]}"
|
||||
security list-keychains -d user -s "$keychain_path" "${existing_keychains[@]}" || true
|
||||
security default-keychain -d user -s "$keychain_path" || true
|
||||
security list-keychains
|
||||
security list-keychains -d user || true
|
||||
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$keychain_password" "$keychain_path"
|
||||
identity_output="$(security find-identity -v -p codesigning "$keychain_path")"
|
||||
printf '%s\n' "$identity_output"
|
||||
security find-identity -v -p codesigning
|
||||
codesign_identity_sha1="$(printf '%s\n' "$identity_output" | awk '/"Developer ID Application:/ { print $2; exit }')"
|
||||
codesign_identity="$(printf '%s\n' "$identity_output" | sed -n 's/.*"\(Developer ID Application:[^"]*\)".*/\1/p' | head -n 1)"
|
||||
if [ -z "$codesign_identity" ]; then
|
||||
echo "Developer ID Application signing identity is required in DEVELOPER_ID_APPLICATION_P12_BASE64" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
probe_binary="$RUNNER_TEMP/codesign-probe"
|
||||
cp /usr/bin/true "$probe_binary"
|
||||
codesign --force --dryrun --sign "$codesign_identity" --keychain "$keychain_path" --options runtime --timestamp "$probe_binary"
|
||||
|
||||
echo "CUSTOM_RELEASE_CODESIGN_IDENTITY=$codesign_identity" >> "$GITHUB_ENV"
|
||||
echo "CUSTOM_RELEASE_CODESIGN_IDENTITY_SHA1=$codesign_identity_sha1" >> "$GITHUB_ENV"
|
||||
env:
|
||||
DEVELOPER_ID_APPLICATION_P12_BASE64: ${{ secrets.DEVELOPER_ID_APPLICATION_P12_BASE64 }}
|
||||
DEVELOPER_ID_APPLICATION_P12_PASSWORD: ${{ secrets.DEVELOPER_ID_APPLICATION_P12_PASSWORD }}
|
||||
|
||||
- name: Build patched Syncthing release
|
||||
run: ./scripts/update-custom-release.sh
|
||||
env:
|
||||
CUSTOM_RELEASE_UPSTREAM_TAG: ${{ github.event.inputs.upstream_tag }}
|
||||
CUSTOM_RELEASE_SUFFIX: ${{ github.event.inputs.suffix }}
|
||||
CUSTOM_RELEASE_PUSH: "1"
|
||||
CUSTOM_RELEASE_PUSH_BRANCH: "0"
|
||||
CUSTOM_RELEASE_REMOTE: origin
|
||||
CUSTOM_RELEASE_BUILDS: "darwin/arm64/zip/1 linux/amd64/tar/0 linux/arm64/tar/0"
|
||||
CUSTOM_RELEASE_CODESIGN_TEAM_ID: "NG5W75WE8U"
|
||||
CUSTOM_RELEASE_REQUIRE_GATEKEEPER_ASSESSMENT: "0"
|
||||
CUSTOM_RELEASE_CREATE_GITEA_RELEASE: "1"
|
||||
CUSTOM_RELEASE_TEA_REPO: felixfoertsch/syncthing
|
||||
|
||||
- name: Delete temporary keychain
|
||||
if: always()
|
||||
run: |
|
||||
if [ -n "${CUSTOM_RELEASE_PREVIOUS_DEFAULT_KEYCHAIN:-}" ] && [ -e "$CUSTOM_RELEASE_PREVIOUS_DEFAULT_KEYCHAIN" ]; then
|
||||
security default-keychain -d user -s "$CUSTOM_RELEASE_PREVIOUS_DEFAULT_KEYCHAIN" || true
|
||||
fi
|
||||
if [ -n "${CUSTOM_RELEASE_KEYCHAIN_PATH:-}" ]; then
|
||||
security delete-keychain "$CUSTOM_RELEASE_KEYCHAIN_PATH" || true
|
||||
fi
|
||||
@@ -1,80 +0,0 @@
|
||||
name: custom release
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- ".github/workflows/custom-release.yml"
|
||||
- "patches/**"
|
||||
- "scripts/update-custom-release.sh"
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
upstream_tag:
|
||||
description: "Optional upstream Syncthing tag, for example v2.1.3"
|
||||
required: false
|
||||
suffix:
|
||||
description: "Optional custom release suffix, for example stignore.7"
|
||||
required: false
|
||||
schedule:
|
||||
- cron: "17 04 * * *"
|
||||
|
||||
jobs:
|
||||
build-custom-release:
|
||||
runs-on: macos-14
|
||||
steps:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
cache: false
|
||||
|
||||
- name: Configure Git author
|
||||
run: |
|
||||
git config user.name "GitHub Actions"
|
||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
- name: Build patched Syncthing release
|
||||
run: |
|
||||
case "$(uname -m)" in
|
||||
arm64) darwin_arch=arm64 ;;
|
||||
x86_64) darwin_arch=amd64 ;;
|
||||
*) echo "Unsupported macOS runner architecture: $(uname -m)" >&2; exit 1 ;;
|
||||
esac
|
||||
export CUSTOM_RELEASE_BUILDS="darwin/$darwin_arch/zip/1 linux/amd64/tar/0 linux/arm64/tar/0"
|
||||
./scripts/update-custom-release.sh
|
||||
env:
|
||||
CUSTOM_RELEASE_UPSTREAM_TAG: ${{ github.event.inputs.upstream_tag }}
|
||||
CUSTOM_RELEASE_SUFFIX: ${{ github.event.inputs.suffix }}
|
||||
CUSTOM_RELEASE_PUSH: "1"
|
||||
CUSTOM_RELEASE_PUSH_BRANCH: "0"
|
||||
CUSTOM_RELEASE_REMOTE: origin
|
||||
CUSTOM_RELEASE_CREATE_GITEA_RELEASE: "0"
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
|
||||
- name: Publish GitHub release
|
||||
run: |
|
||||
tag="${CUSTOM_RELEASE_UPSTREAM_TAG:-$(git ls-remote --refs --tags --sort='version:refname' https://github.com/syncthing/syncthing.git 'v[0-9]*' | awk '{ tag = $2; sub("refs/tags/", "", tag); if (tag ~ /^v[0-9]+\.[0-9]+\.[0-9]+$/) latest = tag } END { print latest }')}-$CUSTOM_RELEASE_SUFFIX"
|
||||
if gh release view "$tag" >/dev/null 2>&1; then
|
||||
echo "GitHub release $tag already exists; nothing to do."
|
||||
exit 0
|
||||
fi
|
||||
assets=()
|
||||
for asset in dist/*; do
|
||||
[ -f "$asset" ] || continue
|
||||
[ "$(basename "$asset")" = release-notes.md ] && continue
|
||||
assets+=("$asset")
|
||||
done
|
||||
gh release create "$tag" "${assets[@]}" --title "$tag" --notes-file dist/release-notes.md --verify-tag
|
||||
env:
|
||||
CUSTOM_RELEASE_UPSTREAM_TAG: ${{ github.event.inputs.upstream_tag }}
|
||||
CUSTOM_RELEASE_SUFFIX: ${{ github.event.inputs.suffix || 'stignore.7' }}
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
@@ -18,4 +18,3 @@ deb
|
||||
/repos
|
||||
/proto/scripts/protoc-gen-gosyncthing
|
||||
/compat.json
|
||||
/dist/
|
||||
|
||||
@@ -116,6 +116,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>
|
||||
@@ -216,6 +217,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>
|
||||
@@ -286,6 +288,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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -612,7 +612,7 @@ func saveRelays(file string, relays []*relay) error {
|
||||
for _, relay := range relays {
|
||||
content += relay.uri.String() + "\n"
|
||||
}
|
||||
return os.WriteFile(file, []byte(content), 0o777)
|
||||
return os.WriteFile(file, []byte(content), 0o666)
|
||||
}
|
||||
|
||||
func createTestCertificate() tls.Certificate {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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.45
|
||||
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/pierrec/lz4/v4 v4.1.27
|
||||
github.com/prometheus/client_golang v1.23.2
|
||||
github.com/puzpuzpuz/xsync/v3 v3.5.1
|
||||
github.com/quic-go/quic-go v0.59.0
|
||||
github.com/quic-go/quic-go v0.60.0
|
||||
github.com/rabbitmq/amqp091-go v1.11.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.5
|
||||
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,14 +40,14 @@ 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.55.0
|
||||
golang.org/x/sys v0.45.0
|
||||
golang.org/x/text v0.37.0
|
||||
golang.org/x/crypto v0.53.0
|
||||
golang.org/x/exp v0.0.0-20260611194520-c48552f49976
|
||||
golang.org/x/net v0.56.0
|
||||
golang.org/x/sys v0.46.0
|
||||
golang.org/x/text v0.38.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.52.0
|
||||
sigs.k8s.io/yaml v1.6.0
|
||||
)
|
||||
|
||||
@@ -94,12 +94,12 @@ require (
|
||||
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
|
||||
golang.org/x/mod v0.37.0 // indirect
|
||||
golang.org/x/sync v0.21.0 // indirect
|
||||
golang.org/x/telemetry v0.0.0-20260610154732-fb80ec83bdd9 // indirect
|
||||
golang.org/x/tools v0.46.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
modernc.org/libc v1.72.0 // indirect
|
||||
modernc.org/libc v1.72.3 // 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
|
||||
|
||||
@@ -19,8 +19,6 @@ 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=
|
||||
@@ -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.45 h1:6KA/spDguL3KV8rnybG7ezSaE4SeMR3KC9VbUoAQaIk=
|
||||
github.com/mattn/go-sqlite3 v1.14.45/go.mod h1:pjEuOr8IwzLJP2MfGeTb0A35jauH+C2kbHKBr7yXKVQ=
|
||||
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=
|
||||
@@ -200,8 +200,10 @@ github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzM
|
||||
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
|
||||
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/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.11.0 h1:HxIctVm9Gid/Vtn706necmZ7Wj6pgGI2eqplRbEY8O8=
|
||||
github.com/rabbitmq/amqp091-go v1.11.0/go.mod h1:Hy4jKW5kQART1u+JkDTF9YYOQUHXqMuhrgxOEeS7G4o=
|
||||
github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 h1:bsUq1dX0N8AOIL7EB/X911+m4EHsnWEHeJ0c+3TTBrg=
|
||||
@@ -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.5 h1:RPcBXkpz7kOj9PqGFQOlBPZHsyaPvPVQc098y9RmCNM=
|
||||
github.com/shirou/gopsutil/v4 v4.26.5/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=
|
||||
@@ -265,13 +267,13 @@ 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.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto=
|
||||
golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio=
|
||||
golang.org/x/exp v0.0.0-20260611194520-c48552f49976 h1:X8Hz2ImujgbmetVuW+w2YkyZChE3cBpZi2P158rTG9M=
|
||||
golang.org/x/exp v0.0.0-20260611194520-c48552f49976/go.mod h1:vnf4pv9iKZXY58sQE1L86zmNWJ4159e1RkcWiLCkeEY=
|
||||
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.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ=
|
||||
golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0=
|
||||
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.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
|
||||
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
|
||||
golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
|
||||
golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
|
||||
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.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
|
||||
golang.org/x/sync v0.21.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.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
|
||||
golang.org/x/sys v0.45.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.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
|
||||
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/telemetry v0.0.0-20260610154732-fb80ec83bdd9 h1:FjUup8XrRy7lv+XHONi6KKUSizeF2NnVrTnz/HhbohQ=
|
||||
golang.org/x/telemetry v0.0.0-20260610154732-fb80ec83bdd9/go.mod h1:3AWMyWHS+caVoiEXpiq6+tzKA40J4vQT3MYr80ZtQpc=
|
||||
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.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE=
|
||||
golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4=
|
||||
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.46.0 h1:7jTurBkPZu4moS/Uy4OQT1M+QBlsj3wejyZwsT8Z7rk=
|
||||
golang.org/x/tools v0.46.0/go.mod h1:FrD85F8l+NWL+9XWBSyVSHO6Ne4jutsfIFba7AWQ5Ys=
|
||||
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,10 +360,10 @@ 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.28.2 h1:3tQ0lf2ADtoby2EtSP+J7IE2SHwEJdP8ioR59wx7XpY=
|
||||
modernc.org/cc/v4 v4.28.2/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI=
|
||||
modernc.org/ccgo/v4 v4.34.0 h1:yRLPFZieg532OT4rp4JFNIVcquwalMX26G95WQDqwCQ=
|
||||
modernc.org/ccgo/v4 v4.34.0/go.mod h1:AS5WYMyBakQ+fhsHhtP8mWB82KTGPkNNJDGfGQCe0/A=
|
||||
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=
|
||||
@@ -370,18 +372,18 @@ 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/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.72.3 h1:ZnDF4tXn4NBXFutMMQC4vtbTFSXhhKzR73fv0beZEAU=
|
||||
modernc.org/libc v1.72.3/go.mod h1:dn0dZNnnn1clLyvRxLxYExxiKRZIRENOfqQ8XEeg4Qs=
|
||||
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.52.0 h1:p4dhYh2tXZCiyaqHwRVJDjIGKWyXayiQpThxgDzJaxo=
|
||||
modernc.org/sqlite v1.52.0/go.mod h1:tcNzv5p84E0skkmJn038y+hWJbLQXQqEnQfeh5r2JLM=
|
||||
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=
|
||||
|
||||
@@ -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}}“",
|
||||
|
||||
@@ -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 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, 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, 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, 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, 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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -798,6 +798,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)
|
||||
@@ -825,6 +832,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)
|
||||
@@ -837,6 +858,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()
|
||||
|
||||
|
||||
@@ -235,6 +235,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(`
|
||||
|
||||
+1
-1
@@ -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))
|
||||
}
|
||||
|
||||
|
||||
@@ -168,7 +168,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 +176,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 +246,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
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
+4
-19
@@ -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) {
|
||||
@@ -241,15 +241,7 @@ func (f *BasicFilesystem) DirNames(name string) ([]string, error) {
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
func (f *BasicFilesystem) OpenFile(name string, flags int, mode FileMode) (File, error) {
|
||||
@@ -257,6 +249,7 @@ func (f *BasicFilesystem) OpenFile(name string, flags int, mode FileMode) (File,
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
flags |= alwaysOpenFlags // enforce extra bits in flags
|
||||
fd, err := os.OpenFile(rootedName, flags, os.FileMode(mode))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -265,15 +258,7 @@ func (f *BasicFilesystem) OpenFile(name string, flags int, mode FileMode) (File,
|
||||
}
|
||||
|
||||
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 {
|
||||
|
||||
@@ -14,8 +14,11 @@ import (
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
const alwaysOpenFlags = syscall.O_NOFOLLOW // never open symlinks as the final path component
|
||||
|
||||
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 {
|
||||
|
||||
@@ -21,6 +21,8 @@ import (
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
const alwaysOpenFlags = 0 // no extra flags
|
||||
|
||||
var errNotSupported = errors.New("symlinks not supported")
|
||||
|
||||
func (BasicFilesystem) ReadSymlink(path string) (string, error) {
|
||||
@@ -31,57 +33,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 {
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
@@ -284,8 +284,8 @@ func NewFilesystem(fsType FilesystemType, uri string, opts ...Option) Filesystem
|
||||
}
|
||||
|
||||
// fs cannot import config or versioner, so we hard code .stfolder
|
||||
// (config.DefaultMarkerName) and .stversions (versioner.DefaultPath).
|
||||
var internals = []string{".stfolder", ".stversions"}
|
||||
// (config.DefaultMarkerName) and .stversions (versioner.DefaultPath)
|
||||
var internals = []string{".stfolder", ".stignore", ".stversions"}
|
||||
|
||||
// IsInternal returns true if the file, as a path relative to the folder
|
||||
// root, represents an internal file that should always be ignored. The file
|
||||
|
||||
@@ -21,8 +21,10 @@ func TestIsInternal(t *testing.T) {
|
||||
internal bool
|
||||
}{
|
||||
{".stfolder", true},
|
||||
{".stignore", true},
|
||||
{".stversions", true},
|
||||
{".stfolder/foo", true},
|
||||
{".stignore/foo", true},
|
||||
{".stversions/foo", true},
|
||||
|
||||
{".stfolderfoo", false},
|
||||
@@ -32,8 +34,6 @@ func TestIsInternal(t *testing.T) {
|
||||
{"foo.stignore", false},
|
||||
{"foo.stversions", false},
|
||||
{"foo/.stfolder", false},
|
||||
{".stignore", false},
|
||||
{".stignore/foo", false},
|
||||
{"foo/.stignore", false},
|
||||
{"foo/.stversions", false},
|
||||
}
|
||||
|
||||
@@ -60,15 +60,15 @@ func TestRecvOnlyRevertDeletes(t *testing.T) {
|
||||
|
||||
must(t, m.ScanFolder("ro"))
|
||||
|
||||
// We should now have three files and two directories, with global state unchanged.
|
||||
// We should now have two files and two directories, with global state unchanged.
|
||||
|
||||
size = mustV(m.GlobalSize("ro"))
|
||||
if size.Files != 1 || size.Directories != 1 {
|
||||
t.Fatalf("Global: expected 1 file and 1 directory: %+v", size)
|
||||
}
|
||||
size = mustV(m.LocalSize("ro", protocol.LocalDeviceID))
|
||||
if size.Files != 3 || size.Directories != 2 {
|
||||
t.Fatalf("Local: expected 3 files and 2 directories: %+v", size)
|
||||
if size.Files != 2 || size.Directories != 2 {
|
||||
t.Fatalf("Local: expected 2 files and 2 directories: %+v", size)
|
||||
}
|
||||
size = mustV(m.ReceiveOnlySize("ro"))
|
||||
if size.Files+size.Directories == 0 {
|
||||
|
||||
@@ -382,7 +382,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 +701,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 +1345,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 +1540,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
|
||||
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
+20
-9
@@ -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")
|
||||
}
|
||||
@@ -2979,15 +2989,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)
|
||||
}
|
||||
|
||||
@@ -974,15 +974,13 @@ func TestIgnoreDeleteUnignore(t *testing.T) {
|
||||
file := "foobar"
|
||||
contents := []byte("test file contents\n")
|
||||
|
||||
basicCheck := func(fs []protocol.FileInfo) protocol.FileInfo {
|
||||
basicCheck := func(fs []protocol.FileInfo) {
|
||||
t.Helper()
|
||||
for _, f := range fs {
|
||||
if f.Name == file {
|
||||
return f
|
||||
}
|
||||
if len(fs) != 1 {
|
||||
t.Fatal("expected a single index entry, got", len(fs))
|
||||
} else if fs[0].Name != file {
|
||||
t.Fatalf("expected a index entry for %v, got one for %v", file, fs[0].Name)
|
||||
}
|
||||
t.Fatalf("expected an index entry for %v, got %v", file, fs)
|
||||
return protocol.FileInfo{}
|
||||
}
|
||||
|
||||
done := make(chan struct{})
|
||||
@@ -1003,7 +1001,8 @@ func TestIgnoreDeleteUnignore(t *testing.T) {
|
||||
|
||||
done = make(chan struct{})
|
||||
fc.setIndexFn(func(_ context.Context, folder string, fs []protocol.FileInfo) error {
|
||||
f := basicCheck(fs)
|
||||
basicCheck(fs)
|
||||
f := fs[0]
|
||||
if !f.IsInvalid() {
|
||||
t.Errorf("Received non-invalid index update")
|
||||
}
|
||||
@@ -1023,7 +1022,8 @@ func TestIgnoreDeleteUnignore(t *testing.T) {
|
||||
|
||||
done = make(chan struct{})
|
||||
fc.setIndexFn(func(_ context.Context, folder string, fs []protocol.FileInfo) error {
|
||||
f := basicCheck(fs)
|
||||
basicCheck(fs)
|
||||
f := fs[0]
|
||||
if f.IsInvalid() {
|
||||
t.Errorf("Received invalid index update")
|
||||
}
|
||||
|
||||
+13
-19
@@ -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.
|
||||
|
||||
+28
-13
@@ -68,15 +68,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 +484,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 +636,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 != SyntheticDirectorySize:
|
||||
// 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
|
||||
|
||||
@@ -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: SyntheticDirectorySize,
|
||||
},
|
||||
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: SyntheticDirectorySize,
|
||||
},
|
||||
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
@@ -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) {}
|
||||
|
||||
@@ -40,7 +40,6 @@ type testfile struct {
|
||||
type testfileList []testfile
|
||||
|
||||
var testdata = testfileList{
|
||||
{".stignore", 48, "f60db5c36f642f8a6c1a636024330cd4dba6ab965083542f3629ff7d8c547911"},
|
||||
{"afile", 4, "b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c"},
|
||||
{"dir1", 128, ""},
|
||||
{filepath.Join("dir1", "dfile"), 5, "49ae93732fcf8d63fe1cce759664982dbd5b23161f007dba8561862adc96d063"},
|
||||
|
||||
@@ -155,7 +155,7 @@ func archiveFile(method fs.CopyRangeMethod, srcFs, dstFs fs.Filesystem, filePath
|
||||
if err != nil {
|
||||
if fs.IsNotExist(err) {
|
||||
slog.Debug("Creating versions dir")
|
||||
err := dstFs.MkdirAll(".", 0o755)
|
||||
err := dstFs.MkdirAll(".", fs.ModePerm)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -328,7 +328,7 @@ func restoreFile(method fs.CopyRangeMethod, src, dst fs.Filesystem, filePath str
|
||||
return err
|
||||
}
|
||||
|
||||
_ = dst.MkdirAll(filepath.Dir(filePath), 0o755)
|
||||
_ = dst.MkdirAll(filepath.Dir(filePath), fs.ModePerm)
|
||||
err := osutil.RenameOrCopy(method, src, dst, sourceFile, filePath)
|
||||
_ = dst.Chtimes(filePath, sourceMtime, sourceMtime)
|
||||
return err
|
||||
|
||||
+1
-1
@@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
|
||||
.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
|
||||
.in \\n[rst2man-indent\\n[rst2man-indent-level]]u
|
||||
..
|
||||
.TH "STDISCOSRV" "1" "May 13, 2026" "v2.1.0" "Syncthing"
|
||||
.TH "STDISCOSRV" "1" "Jun 07, 2026" "v2.1.0" "Syncthing"
|
||||
.SH NAME
|
||||
stdiscosrv \- Syncthing Discovery Server
|
||||
.SH SYNOPSIS
|
||||
|
||||
+1
-1
@@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
|
||||
.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
|
||||
.in \\n[rst2man-indent\\n[rst2man-indent-level]]u
|
||||
..
|
||||
.TH "STRELAYSRV" "1" "May 13, 2026" "v2.1.0" "Syncthing"
|
||||
.TH "STRELAYSRV" "1" "Jun 07, 2026" "v2.1.0" "Syncthing"
|
||||
.SH NAME
|
||||
strelaysrv \- Syncthing Relay Server
|
||||
.SH SYNOPSIS
|
||||
|
||||
+1
-1
@@ -28,7 +28,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
|
||||
.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
|
||||
.in \\n[rst2man-indent\\n[rst2man-indent-level]]u
|
||||
..
|
||||
.TH "SYNCTHING-BEP" "7" "May 13, 2026" "v2.1.0" "Syncthing"
|
||||
.TH "SYNCTHING-BEP" "7" "Jun 07, 2026" "v2.1.0" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-bep \- Block Exchange Protocol v1
|
||||
.SH INTRODUCTION AND DEFINITIONS
|
||||
|
||||
@@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
|
||||
.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
|
||||
.in \\n[rst2man-indent\\n[rst2man-indent-level]]u
|
||||
..
|
||||
.TH "SYNCTHING-CONFIG" "5" "May 13, 2026" "v2.1.0" "Syncthing"
|
||||
.TH "SYNCTHING-CONFIG" "5" "Jun 07, 2026" "v2.1.0" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-config \- Syncthing Configuration
|
||||
.SH OVERVIEW
|
||||
|
||||
@@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
|
||||
.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
|
||||
.in \\n[rst2man-indent\\n[rst2man-indent-level]]u
|
||||
..
|
||||
.TH "SYNCTHING-DEVICE-IDS" "7" "May 13, 2026" "v2.1.0" "Syncthing"
|
||||
.TH "SYNCTHING-DEVICE-IDS" "7" "Jun 07, 2026" "v2.1.0" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-device-ids \- Understanding Device IDs
|
||||
.sp
|
||||
|
||||
@@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
|
||||
.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
|
||||
.in \\n[rst2man-indent\\n[rst2man-indent-level]]u
|
||||
..
|
||||
.TH "SYNCTHING-EVENT-API" "7" "May 13, 2026" "v2.1.0" "Syncthing"
|
||||
.TH "SYNCTHING-EVENT-API" "7" "Jun 07, 2026" "v2.1.0" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-event-api \- Event API
|
||||
.SH DESCRIPTION
|
||||
|
||||
+1
-1
@@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
|
||||
.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
|
||||
.in \\n[rst2man-indent\\n[rst2man-indent-level]]u
|
||||
..
|
||||
.TH "SYNCTHING-FAQ" "7" "May 13, 2026" "v2.1.0" "Syncthing"
|
||||
.TH "SYNCTHING-FAQ" "7" "Jun 07, 2026" "v2.1.0" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-faq \- Frequently Asked Questions
|
||||
.INDENT 0.0
|
||||
|
||||
@@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
|
||||
.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
|
||||
.in \\n[rst2man-indent\\n[rst2man-indent-level]]u
|
||||
..
|
||||
.TH "SYNCTHING-GLOBALDISCO" "7" "May 13, 2026" "v2.1.0" "Syncthing"
|
||||
.TH "SYNCTHING-GLOBALDISCO" "7" "Jun 07, 2026" "v2.1.0" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-globaldisco \- Global Discovery Protocol v3
|
||||
.SH ANNOUNCEMENTS
|
||||
|
||||
@@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
|
||||
.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
|
||||
.in \\n[rst2man-indent\\n[rst2man-indent-level]]u
|
||||
..
|
||||
.TH "SYNCTHING-LOCALDISCO" "7" "May 13, 2026" "v2.1.0" "Syncthing"
|
||||
.TH "SYNCTHING-LOCALDISCO" "7" "Jun 07, 2026" "v2.1.0" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-localdisco \- Local Discovery Protocol v4
|
||||
.SH MODE OF OPERATION
|
||||
|
||||
@@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
|
||||
.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
|
||||
.in \\n[rst2man-indent\\n[rst2man-indent-level]]u
|
||||
..
|
||||
.TH "SYNCTHING-NETWORKING" "7" "May 13, 2026" "v2.1.0" "Syncthing"
|
||||
.TH "SYNCTHING-NETWORKING" "7" "Jun 07, 2026" "v2.1.0" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-networking \- Firewall Setup
|
||||
.SH ROUTER SETUP
|
||||
|
||||
@@ -28,7 +28,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
|
||||
.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
|
||||
.in \\n[rst2man-indent\\n[rst2man-indent-level]]u
|
||||
..
|
||||
.TH "SYNCTHING-RELAY" "7" "May 13, 2026" "v2.1.0" "Syncthing"
|
||||
.TH "SYNCTHING-RELAY" "7" "Jun 07, 2026" "v2.1.0" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-relay \- Relay Protocol v1
|
||||
.SH WHAT IS A RELAY?
|
||||
|
||||
@@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
|
||||
.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
|
||||
.in \\n[rst2man-indent\\n[rst2man-indent-level]]u
|
||||
..
|
||||
.TH "SYNCTHING-REST-API" "7" "May 13, 2026" "v2.1.0" "Syncthing"
|
||||
.TH "SYNCTHING-REST-API" "7" "Jun 07, 2026" "v2.1.0" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-rest-api \- REST API
|
||||
.sp
|
||||
|
||||
@@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
|
||||
.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
|
||||
.in \\n[rst2man-indent\\n[rst2man-indent-level]]u
|
||||
..
|
||||
.TH "SYNCTHING-SECURITY" "7" "May 13, 2026" "v2.1.0" "Syncthing"
|
||||
.TH "SYNCTHING-SECURITY" "7" "Jun 07, 2026" "v2.1.0" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-security \- Security Principles
|
||||
.sp
|
||||
|
||||
@@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
|
||||
.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
|
||||
.in \\n[rst2man-indent\\n[rst2man-indent-level]]u
|
||||
..
|
||||
.TH "SYNCTHING-STIGNORE" "5" "May 13, 2026" "v2.1.0" "Syncthing"
|
||||
.TH "SYNCTHING-STIGNORE" "5" "Jun 07, 2026" "v2.1.0" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-stignore \- Prevent files from being synchronized to other nodes
|
||||
.SH SYNOPSIS
|
||||
|
||||
@@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
|
||||
.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
|
||||
.in \\n[rst2man-indent\\n[rst2man-indent-level]]u
|
||||
..
|
||||
.TH "SYNCTHING-VERSIONING" "7" "May 13, 2026" "v2.1.0" "Syncthing"
|
||||
.TH "SYNCTHING-VERSIONING" "7" "Jun 07, 2026" "v2.1.0" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-versioning \- Keep automatic backups of deleted files by other nodes
|
||||
.sp
|
||||
|
||||
+1
-1
@@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
|
||||
.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
|
||||
.in \\n[rst2man-indent\\n[rst2man-indent-level]]u
|
||||
..
|
||||
.TH "SYNCTHING" "1" "May 13, 2026" "v2.1.0" "Syncthing"
|
||||
.TH "SYNCTHING" "1" "Jun 07, 2026" "v2.1.0" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing \- Syncthing
|
||||
.SH SYNOPSIS
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
# Local Syncthing Patches
|
||||
|
||||
Apply the local patches manually after pulling a new upstream Syncthing release:
|
||||
|
||||
```bash
|
||||
git apply patches/sync-stignore.patch patches/webui-build-marker.patch
|
||||
go run build.go -build-out bin/syncthing-stignore build syncthing
|
||||
```
|
||||
|
||||
The automated release flow uses:
|
||||
|
||||
```bash
|
||||
./scripts/update-custom-release.sh
|
||||
```
|
||||
|
||||
By default the script finds the latest stable upstream tag, creates a local
|
||||
`custom/<version>-<suffix>` branch, applies all local patches, removes upstream
|
||||
GitHub/Gitea workflow files from the release commit, tags
|
||||
`<upstream>-stignore.7`, regenerates embedded GUI assets, runs focused tests,
|
||||
and writes build artifacts to `dist/`.
|
||||
|
||||
Useful options:
|
||||
|
||||
```bash
|
||||
CUSTOM_RELEASE_UPSTREAM_TAG=v2.1.0 ./scripts/update-custom-release.sh
|
||||
CUSTOM_RELEASE_FORCE=1 ./scripts/update-custom-release.sh
|
||||
CUSTOM_RELEASE_PATCHES="patches/sync-stignore.patch patches/webui-build-marker.patch" ./scripts/update-custom-release.sh
|
||||
CUSTOM_RELEASE_PUSH=1 CUSTOM_RELEASE_REMOTE=gitea ./scripts/update-custom-release.sh
|
||||
CUSTOM_RELEASE_PUSH=1 CUSTOM_RELEASE_PUSH_BRANCH=1 CUSTOM_RELEASE_REMOTE=gitea ./scripts/update-custom-release.sh
|
||||
CUSTOM_RELEASE_CREATE_GITEA_RELEASE=1 CUSTOM_RELEASE_TEA_REPO=felixfoertsch/syncthing ./scripts/update-custom-release.sh
|
||||
CUSTOM_RELEASE_BUILDS="darwin/amd64/zip darwin/arm64/zip linux/amd64/tar linux/arm64/tar" ./scripts/update-custom-release.sh
|
||||
```
|
||||
|
||||
The Gitea and GitHub workflows run the script on their respective CI hosts when
|
||||
the local patchset changes on `main`, on a schedule, and on manual dispatch.
|
||||
The repository's `upstream` branch stays a clean upstream mirror.
|
||||
The workflow detects the latest upstream Syncthing stable tag, exits if the
|
||||
matching custom tag already exists, pushes only the `<upstream>-stignore.7` tag
|
||||
by default, and publishes release assets on the CI host running the workflow.
|
||||
The local `custom/<version>` branch is only pushed when
|
||||
`CUSTOM_RELEASE_PUSH_BRANCH=1` is set. The workflow builds macOS arm64, Linux
|
||||
amd64, and Linux arm64 archives.
|
||||
|
||||
The patch makes root-level `.stignore` sync like regular folder content while
|
||||
keeping `.stfolder` and `.stversions` protected as internal Syncthing paths.
|
||||
The web UI patch adds `It syncs .stignore now!` to the GUI footer so the custom
|
||||
binary is visually distinguishable from upstream builds. The release test fails
|
||||
if the generated GUI asset bundle does not contain that marker.
|
||||
@@ -1,119 +0,0 @@
|
||||
diff --git a/lib/fs/filesystem.go b/lib/fs/filesystem.go
|
||||
index 31bbb9770..dcece778e 100644
|
||||
--- a/lib/fs/filesystem.go
|
||||
+++ b/lib/fs/filesystem.go
|
||||
@@ -284,8 +284,8 @@ func NewFilesystem(fsType FilesystemType, uri string, opts ...Option) Filesystem
|
||||
}
|
||||
|
||||
// fs cannot import config or versioner, so we hard code .stfolder
|
||||
-// (config.DefaultMarkerName) and .stversions (versioner.DefaultPath)
|
||||
-var internals = []string{".stfolder", ".stignore", ".stversions"}
|
||||
+// (config.DefaultMarkerName) and .stversions (versioner.DefaultPath).
|
||||
+var internals = []string{".stfolder", ".stversions"}
|
||||
|
||||
// IsInternal returns true if the file, as a path relative to the folder
|
||||
// root, represents an internal file that should always be ignored. The file
|
||||
diff --git a/lib/fs/filesystem_test.go b/lib/fs/filesystem_test.go
|
||||
index 08e45ff50..2d100a2b0 100644
|
||||
--- a/lib/fs/filesystem_test.go
|
||||
+++ b/lib/fs/filesystem_test.go
|
||||
@@ -21,10 +21,8 @@ func TestIsInternal(t *testing.T) {
|
||||
internal bool
|
||||
}{
|
||||
{".stfolder", true},
|
||||
- {".stignore", true},
|
||||
{".stversions", true},
|
||||
{".stfolder/foo", true},
|
||||
- {".stignore/foo", true},
|
||||
{".stversions/foo", true},
|
||||
|
||||
{".stfolderfoo", false},
|
||||
@@ -34,6 +32,8 @@ func TestIsInternal(t *testing.T) {
|
||||
{"foo.stignore", false},
|
||||
{"foo.stversions", false},
|
||||
{"foo/.stfolder", false},
|
||||
+ {".stignore", false},
|
||||
+ {".stignore/foo", false},
|
||||
{"foo/.stignore", false},
|
||||
{"foo/.stversions", false},
|
||||
}
|
||||
diff --git a/lib/model/folder_recvonly_test.go b/lib/model/folder_recvonly_test.go
|
||||
index e84d0c49d..fbd442966 100644
|
||||
--- a/lib/model/folder_recvonly_test.go
|
||||
+++ b/lib/model/folder_recvonly_test.go
|
||||
@@ -60,15 +60,15 @@ func TestRecvOnlyRevertDeletes(t *testing.T) {
|
||||
|
||||
must(t, m.ScanFolder("ro"))
|
||||
|
||||
- // We should now have two files and two directories, with global state unchanged.
|
||||
+ // We should now have three files and two directories, with global state unchanged.
|
||||
|
||||
size = mustV(m.GlobalSize("ro"))
|
||||
if size.Files != 1 || size.Directories != 1 {
|
||||
t.Fatalf("Global: expected 1 file and 1 directory: %+v", size)
|
||||
}
|
||||
size = mustV(m.LocalSize("ro", protocol.LocalDeviceID))
|
||||
- if size.Files != 2 || size.Directories != 2 {
|
||||
- t.Fatalf("Local: expected 2 files and 2 directories: %+v", size)
|
||||
+ if size.Files != 3 || size.Directories != 2 {
|
||||
+ t.Fatalf("Local: expected 3 files and 2 directories: %+v", size)
|
||||
}
|
||||
size = mustV(m.ReceiveOnlySize("ro"))
|
||||
if size.Files+size.Directories == 0 {
|
||||
diff --git a/lib/model/requests_test.go b/lib/model/requests_test.go
|
||||
index 288140721..6f8124e97 100644
|
||||
--- a/lib/model/requests_test.go
|
||||
+++ b/lib/model/requests_test.go
|
||||
@@ -974,13 +974,15 @@ func TestIgnoreDeleteUnignore(t *testing.T) {
|
||||
file := "foobar"
|
||||
contents := []byte("test file contents\n")
|
||||
|
||||
- basicCheck := func(fs []protocol.FileInfo) {
|
||||
+ basicCheck := func(fs []protocol.FileInfo) protocol.FileInfo {
|
||||
t.Helper()
|
||||
- if len(fs) != 1 {
|
||||
- t.Fatal("expected a single index entry, got", len(fs))
|
||||
- } else if fs[0].Name != file {
|
||||
- t.Fatalf("expected a index entry for %v, got one for %v", file, fs[0].Name)
|
||||
+ for _, f := range fs {
|
||||
+ if f.Name == file {
|
||||
+ return f
|
||||
+ }
|
||||
}
|
||||
+ t.Fatalf("expected an index entry for %v, got %v", file, fs)
|
||||
+ return protocol.FileInfo{}
|
||||
}
|
||||
|
||||
done := make(chan struct{})
|
||||
@@ -1001,8 +1003,7 @@ func TestIgnoreDeleteUnignore(t *testing.T) {
|
||||
|
||||
done = make(chan struct{})
|
||||
fc.setIndexFn(func(_ context.Context, folder string, fs []protocol.FileInfo) error {
|
||||
- basicCheck(fs)
|
||||
- f := fs[0]
|
||||
+ f := basicCheck(fs)
|
||||
if !f.IsInvalid() {
|
||||
t.Errorf("Received non-invalid index update")
|
||||
}
|
||||
@@ -1022,8 +1023,7 @@ func TestIgnoreDeleteUnignore(t *testing.T) {
|
||||
|
||||
done = make(chan struct{})
|
||||
fc.setIndexFn(func(_ context.Context, folder string, fs []protocol.FileInfo) error {
|
||||
- basicCheck(fs)
|
||||
- f := fs[0]
|
||||
+ f := basicCheck(fs)
|
||||
if f.IsInvalid() {
|
||||
t.Errorf("Received invalid index update")
|
||||
}
|
||||
diff --git a/lib/scanner/walk_test.go b/lib/scanner/walk_test.go
|
||||
index d75dddaf3..3693ae4db 100644
|
||||
--- a/lib/scanner/walk_test.go
|
||||
+++ b/lib/scanner/walk_test.go
|
||||
@@ -40,6 +40,7 @@ type testfile struct {
|
||||
type testfileList []testfile
|
||||
|
||||
var testdata = testfileList{
|
||||
+ {".stignore", 48, "f60db5c36f642f8a6c1a636024330cd4dba6ab965083542f3629ff7d8c547911"},
|
||||
{"afile", 4, "b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c"},
|
||||
{"dir1", 128, ""},
|
||||
{filepath.Join("dir1", "dfile"), 5, "49ae93732fcf8d63fe1cce759664982dbd5b23161f007dba8561862adc96d063"},
|
||||
@@ -1,62 +0,0 @@
|
||||
diff --git a/gui/default/index.html b/gui/default/index.html
|
||||
index e3e573c9a..5ec76e740 100644
|
||||
--- a/gui/default/index.html
|
||||
+++ b/gui/default/index.html
|
||||
@@ -1017,6 +1017,9 @@
|
||||
</div> <!-- /row -->
|
||||
|
||||
</div> <!-- /container -->
|
||||
+ <footer class="container text-center text-muted small" aria-label="Custom build marker">
|
||||
+ It syncs .stignore now!
|
||||
+ </footer>
|
||||
</div> <!-- /ng-cloak -->
|
||||
|
||||
<ng-include src="'syncthing/core/networkErrorDialogView.html'"></ng-include>
|
||||
diff --git a/lib/api/auto/custom_marker_test.go b/lib/api/auto/custom_marker_test.go
|
||||
new file mode 100644
|
||||
index 000000000..73eae3db7
|
||||
--- /dev/null
|
||||
+++ b/lib/api/auto/custom_marker_test.go
|
||||
@@ -0,0 +1,42 @@
|
||||
+// 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/.
|
||||
+
|
||||
+package auto
|
||||
+
|
||||
+import (
|
||||
+ "compress/gzip"
|
||||
+ "io"
|
||||
+ "strings"
|
||||
+ "testing"
|
||||
+)
|
||||
+
|
||||
+const customBuildMarker = "It syncs .stignore now!"
|
||||
+
|
||||
+func TestCustomBuildMarkerIsEmbedded(t *testing.T) {
|
||||
+ asset, ok := Assets()["default/index.html"]
|
||||
+ if !ok {
|
||||
+ t.Fatal("default/index.html is missing from embedded GUI assets")
|
||||
+ }
|
||||
+
|
||||
+ content := asset.Content
|
||||
+ if asset.Gzipped {
|
||||
+ reader, err := gzip.NewReader(strings.NewReader(asset.Content))
|
||||
+ if err != nil {
|
||||
+ t.Fatal(err)
|
||||
+ }
|
||||
+ defer reader.Close()
|
||||
+
|
||||
+ data, err := io.ReadAll(reader)
|
||||
+ if err != nil {
|
||||
+ t.Fatal(err)
|
||||
+ }
|
||||
+ content = string(data)
|
||||
+ }
|
||||
+
|
||||
+ if !strings.Contains(content, customBuildMarker) {
|
||||
+ t.Fatalf("embedded GUI assets do not contain custom build marker %q", customBuildMarker)
|
||||
+ }
|
||||
+}
|
||||
+1
-1
@@ -93,7 +93,7 @@ func main() {
|
||||
}
|
||||
bs = authorsRe.ReplaceAll(bs, []byte("id=\"contributor-list\">\n"+replacement+"\n </div>"))
|
||||
|
||||
if err := os.WriteFile(htmlFile, bs, 0o644); err != nil {
|
||||
if err := os.WriteFile(htmlFile, bs, 0o666); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
|
||||
@@ -265,7 +265,7 @@ func readAll(path string) []byte {
|
||||
}
|
||||
|
||||
func writeFile(path string, data string) {
|
||||
err := os.WriteFile(path, []byte(data), 0o644)
|
||||
err := os.WriteFile(path, []byte(data), 0o666)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -1,195 +0,0 @@
|
||||
#!/usr/bin/env bats
|
||||
|
||||
setup() {
|
||||
REPO_ROOT="$(git rev-parse --show-toplevel)"
|
||||
WORKFLOW="$REPO_ROOT/.gitea/workflows/custom-release.yml"
|
||||
GITHUB_WORKFLOW="$REPO_ROOT/.github/workflows/custom-release.yml"
|
||||
RELEASE_SCRIPT="$REPO_ROOT/scripts/update-custom-release.sh"
|
||||
}
|
||||
|
||||
@test "github custom release uses a github macos runner and github releases" {
|
||||
run rg -n 'runs-on:[[:space:]]*macos-14' "$GITHUB_WORKFLOW"
|
||||
[ "$status" -eq 0 ]
|
||||
|
||||
run rg -n 'gh release create' "$GITHUB_WORKFLOW"
|
||||
[ "$status" -eq 0 ]
|
||||
|
||||
run rg -n 'CUSTOM_RELEASE_CREATE_GITEA_RELEASE: "0"' "$GITHUB_WORKFLOW"
|
||||
[ "$status" -eq 0 ]
|
||||
|
||||
run rg -n 'tea|ffmini_macos_arm64' "$GITHUB_WORKFLOW"
|
||||
[ "$status" -ne 0 ]
|
||||
}
|
||||
|
||||
@test "custom release runs as one job on ffmini macos runner" {
|
||||
run rg -n 'runs-on:[[:space:]]*ffmini_macos_arm64' "$WORKFLOW"
|
||||
[ "$status" -eq 0 ]
|
||||
|
||||
run rg -n 'ubuntu-latest' "$WORKFLOW"
|
||||
[ "$status" -ne 0 ]
|
||||
|
||||
run rg -n 'actions/upload-artifact' "$WORKFLOW"
|
||||
[ "$status" -ne 0 ]
|
||||
}
|
||||
|
||||
@test "custom release tea login setup is idempotent on persistent host runner" {
|
||||
run rg -n 'tea" logins delete actions >/dev/null 2>&1 \|\| true' "$WORKFLOW"
|
||||
[ "$status" -eq 0 ]
|
||||
|
||||
run rg -n 'tea" logins add --name actions' "$WORKFLOW"
|
||||
[ "$status" -eq 0 ]
|
||||
}
|
||||
|
||||
@test "custom release workflow imports developer id signing material into temporary keychain" {
|
||||
run rg -n 'DEVELOPER_ID_APPLICATION_P12_BASE64' "$WORKFLOW"
|
||||
[ "$status" -eq 0 ]
|
||||
|
||||
run rg -n 'DEVELOPER_ID_APPLICATION_P12_PASSWORD' "$WORKFLOW"
|
||||
[ "$status" -eq 0 ]
|
||||
|
||||
run rg -n 'DEVELOPER_ID_APPLICATION_P12_BASE64 secret is required' "$WORKFLOW"
|
||||
[ "$status" -eq 0 ]
|
||||
|
||||
run rg -n 'DEVELOPER_ID_APPLICATION_P12_PASSWORD secret is required' "$WORKFLOW"
|
||||
[ "$status" -ne 0 ]
|
||||
|
||||
run rg -n 'security import .* -P "\$DEVELOPER_ID_APPLICATION_P12_PASSWORD"' "$WORKFLOW"
|
||||
[ "$status" -eq 0 ]
|
||||
|
||||
run rg -n 'security import .* -A ' "$WORKFLOW"
|
||||
[ "$status" -eq 0 ]
|
||||
|
||||
run rg -n 'security find-identity -v -p codesigning "\$keychain_path"' "$WORKFLOW"
|
||||
[ "$status" -eq 0 ]
|
||||
|
||||
run rg -n 'security default-keychain -d user -s "\$keychain_path"' "$WORKFLOW"
|
||||
[ "$status" -eq 0 ]
|
||||
|
||||
run rg -n 'previous_default_keychain=' "$WORKFLOW"
|
||||
[ "$status" -eq 0 ]
|
||||
|
||||
run rg -n 'CUSTOM_RELEASE_PREVIOUS_DEFAULT_KEYCHAIN=\$previous_default_keychain' "$WORKFLOW"
|
||||
[ "$status" -eq 0 ]
|
||||
|
||||
run rg -n 'CUSTOM_RELEASE_PREVIOUS_DYNAMIC_DEFAULT_KEYCHAIN' "$WORKFLOW"
|
||||
[ "$status" -ne 0 ]
|
||||
|
||||
run rg -n 'security default-keychain -s "\$CUSTOM_RELEASE_PREVIOUS_DEFAULT_KEYCHAIN"' "$WORKFLOW"
|
||||
[ "$status" -ne 0 ]
|
||||
|
||||
run rg -n 'security default-keychain -d dynamic' "$WORKFLOW"
|
||||
[ "$status" -ne 0 ]
|
||||
|
||||
run rg -n 'security find-identity -v -p codesigning$' "$WORKFLOW" "$RELEASE_SCRIPT"
|
||||
[ "$status" -eq 0 ]
|
||||
|
||||
run rg -n 'codesign_identity_sha1=' "$WORKFLOW"
|
||||
[ "$status" -eq 0 ]
|
||||
|
||||
run rg -n 'sed -n .*Developer ID Application' "$WORKFLOW"
|
||||
[ "$status" -eq 0 ]
|
||||
|
||||
run rg -n 'CUSTOM_RELEASE_CODESIGN_IDENTITY=\$codesign_identity' "$WORKFLOW"
|
||||
[ "$status" -eq 0 ]
|
||||
|
||||
run rg -n 'CUSTOM_RELEASE_CODESIGN_IDENTITY_SHA1=\$codesign_identity_sha1' "$WORKFLOW"
|
||||
[ "$status" -eq 0 ]
|
||||
|
||||
run rg -n 'CUSTOM_RELEASE_KEYCHAIN_PASSWORD=\$keychain_password' "$WORKFLOW"
|
||||
[ "$status" -eq 0 ]
|
||||
|
||||
run rg -n 'codesign --force --dryrun --sign "\$codesign_identity" --keychain "\$keychain_path" --options runtime --timestamp "\$probe_binary"' "$WORKFLOW"
|
||||
[ "$status" -eq 0 ]
|
||||
|
||||
run rg -n 'CUSTOM_RELEASE_CODESIGN_IDENTITY: "Developer ID Application' "$WORKFLOW"
|
||||
[ "$status" -ne 0 ]
|
||||
|
||||
run rg -n 'security create-keychain' "$WORKFLOW"
|
||||
[ "$status" -eq 0 ]
|
||||
|
||||
run rg -n 'keychain_dir="\$HOME/Library/Keychains"' "$WORKFLOW"
|
||||
[ "$status" -eq 0 ]
|
||||
|
||||
run rg -n 'rm -f "\$keychain_path"' "$WORKFLOW"
|
||||
[ "$status" -eq 0 ]
|
||||
|
||||
run rg -n 'existing_keychains=\(\)' "$WORKFLOW"
|
||||
[ "$status" -eq 0 ]
|
||||
|
||||
run rg -n 'security list-keychains -s "\$keychain_path"' "$WORKFLOW"
|
||||
[ "$status" -eq 0 ]
|
||||
|
||||
run rg -n 'security list-keychains -d dynamic' "$WORKFLOW"
|
||||
[ "$status" -ne 0 ]
|
||||
|
||||
run rg -n 'security import' "$WORKFLOW"
|
||||
[ "$status" -eq 0 ]
|
||||
|
||||
run rg -n 'security delete-keychain' "$WORKFLOW"
|
||||
[ "$status" -eq 0 ]
|
||||
}
|
||||
|
||||
@test "custom release carries per-target cgo mode" {
|
||||
run rg -n 'darwin/arm64/zip/1' "$WORKFLOW" "$RELEASE_SCRIPT"
|
||||
[ "$status" -eq 0 ]
|
||||
|
||||
run rg -n 'linux/amd64/tar/0' "$WORKFLOW" "$RELEASE_SCRIPT"
|
||||
[ "$status" -eq 0 ]
|
||||
|
||||
run rg -n 'CUSTOM_RELEASE_CGO_ENABLED' "$WORKFLOW"
|
||||
[ "$status" -ne 0 ]
|
||||
}
|
||||
|
||||
@test "custom release signs darwin assets with hardened runtime and timestamp" {
|
||||
run rg -n 'codesign_args=\(--force --sign "\$codesign_identity"\)' "$RELEASE_SCRIPT"
|
||||
[ "$status" -eq 0 ]
|
||||
|
||||
run rg -n -- '--keychain "\$CUSTOM_RELEASE_KEYCHAIN_PATH"' "$RELEASE_SCRIPT"
|
||||
[ "$status" -eq 0 ]
|
||||
|
||||
run rg -n 'codesign_args\+=\(--options runtime --timestamp\)' "$RELEASE_SCRIPT"
|
||||
[ "$status" -eq 0 ]
|
||||
|
||||
run rg -n 'security unlock-keychain -p "\$CUSTOM_RELEASE_KEYCHAIN_PASSWORD" "\$CUSTOM_RELEASE_KEYCHAIN_PATH"' "$RELEASE_SCRIPT"
|
||||
[ "$status" -eq 0 ]
|
||||
|
||||
run rg -n 'security find-identity -v -p codesigning "\$CUSTOM_RELEASE_KEYCHAIN_PATH"' "$RELEASE_SCRIPT"
|
||||
[ "$status" -eq 0 ]
|
||||
|
||||
run awk '
|
||||
/codesign_args=\(--force --sign "\$codesign_identity"\)/ { sign = NR }
|
||||
/codesign_args\+=\(--keychain "\$CUSTOM_RELEASE_KEYCHAIN_PATH"\)/ { keychain = NR }
|
||||
/codesign_args\+=\(--options runtime --timestamp\)/ { options = NR }
|
||||
END { exit !(sign && keychain && options && sign < keychain && keychain < options) }
|
||||
' "$RELEASE_SCRIPT"
|
||||
[ "$status" -eq 0 ]
|
||||
|
||||
run rg -n 'Developer ID Application' "$WORKFLOW" "$RELEASE_SCRIPT"
|
||||
[ "$status" -eq 0 ]
|
||||
|
||||
run rg -n 'CUSTOM_RELEASE_CODESIGN_IDENTITY' "$WORKFLOW" "$RELEASE_SCRIPT"
|
||||
[ "$status" -eq 0 ]
|
||||
}
|
||||
|
||||
@test "custom release validates signed darwin binaries before publishing" {
|
||||
run rg -n 'modernc-sqlite' "$RELEASE_SCRIPT"
|
||||
[ "$status" -eq 0 ]
|
||||
|
||||
run rg -n 'codesign --verify --strict --verbose=2' "$RELEASE_SCRIPT"
|
||||
[ "$status" -eq 0 ]
|
||||
|
||||
run rg -n 'TeamIdentifier=NG5W75WE8U|TeamIdentifier.*NG5W75WE8U' "$RELEASE_SCRIPT"
|
||||
[ "$status" -eq 0 ]
|
||||
|
||||
run rg -n 'CUSTOM_RELEASE_REQUIRE_GATEKEEPER_ASSESSMENT: "0"' "$WORKFLOW"
|
||||
[ "$status" -eq 0 ]
|
||||
|
||||
run rg -n 'require_gatekeeper_assessment="\$\{CUSTOM_RELEASE_REQUIRE_GATEKEEPER_ASSESSMENT:-0\}"' "$RELEASE_SCRIPT"
|
||||
[ "$status" -eq 0 ]
|
||||
|
||||
run rg -n 'if \[\[ "\$require_gatekeeper_assessment" == "1" \]\]; then' "$RELEASE_SCRIPT"
|
||||
[ "$status" -eq 0 ]
|
||||
|
||||
run rg -n 'spctl -a -vv --type execute' "$RELEASE_SCRIPT"
|
||||
[ "$status" -eq 0 ]
|
||||
}
|
||||
@@ -1,416 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Description: create a patched Syncthing release from the latest upstream tag.
|
||||
# Usage: ./scripts/update-custom-release.sh
|
||||
|
||||
upstream_url="${CUSTOM_RELEASE_UPSTREAM_URL:-https://github.com/syncthing/syncthing.git}"
|
||||
upstream_tag="${CUSTOM_RELEASE_UPSTREAM_TAG:-}"
|
||||
suffix="${CUSTOM_RELEASE_SUFFIX:-stignore.7}"
|
||||
branch_prefix="${CUSTOM_RELEASE_BRANCH_PREFIX:-custom}"
|
||||
dist_dir="${CUSTOM_RELEASE_DIST_DIR:-dist}"
|
||||
target="${CUSTOM_RELEASE_TARGET:-syncthing}"
|
||||
archive_kind="${CUSTOM_RELEASE_ARCHIVE:-tar}"
|
||||
build_specs="${CUSTOM_RELEASE_BUILDS:-}"
|
||||
default_cgo_enabled="${CUSTOM_RELEASE_CGO_ENABLED:-0}"
|
||||
codesign_identity="${CUSTOM_RELEASE_CODESIGN_IDENTITY:-}"
|
||||
codesign_team_id="${CUSTOM_RELEASE_CODESIGN_TEAM_ID:-NG5W75WE8U}"
|
||||
require_gatekeeper_assessment="${CUSTOM_RELEASE_REQUIRE_GATEKEEPER_ASSESSMENT:-0}"
|
||||
push_release="${CUSTOM_RELEASE_PUSH:-0}"
|
||||
push_branch="${CUSTOM_RELEASE_PUSH_BRANCH:-0}"
|
||||
push_remote="${CUSTOM_RELEASE_REMOTE:-origin}"
|
||||
run_tests="${CUSTOM_RELEASE_TEST:-1}"
|
||||
force="${CUSTOM_RELEASE_FORCE:-0}"
|
||||
publish_gitea_release="${CUSTOM_RELEASE_CREATE_GITEA_RELEASE:-0}"
|
||||
tea_repo="${CUSTOM_RELEASE_TEA_REPO:-}"
|
||||
patch_tmp_dir=""
|
||||
patch_files=()
|
||||
assets_rebuilt=0
|
||||
|
||||
if [[ -n "${CUSTOM_RELEASE_PATCHES:-}" ]]; then
|
||||
read -r -a patch_files <<< "$CUSTOM_RELEASE_PATCHES"
|
||||
elif [[ -n "${CUSTOM_RELEASE_PATCH:-}" ]]; then
|
||||
patch_files=("$CUSTOM_RELEASE_PATCH")
|
||||
else
|
||||
patch_files=(
|
||||
patches/sync-stignore.patch
|
||||
patches/webui-build-marker.patch
|
||||
)
|
||||
fi
|
||||
|
||||
log() {
|
||||
printf '%s\n' "$*"
|
||||
}
|
||||
|
||||
die() {
|
||||
printf 'error: %s\n' "$*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
require_clean_worktree() {
|
||||
if [[ -n "$(git status --porcelain)" ]]; then
|
||||
die "working tree has uncommitted changes"
|
||||
fi
|
||||
}
|
||||
|
||||
latest_stable_tag() {
|
||||
git ls-remote --refs --tags --sort='version:refname' "$upstream_url" 'v[0-9]*' \
|
||||
| awk '{ tag = $2; sub("refs/tags/", "", tag); if (tag ~ /^v[0-9]+\.[0-9]+\.[0-9]+$/) latest = tag } END { print latest }'
|
||||
}
|
||||
|
||||
tag_exists() {
|
||||
local tag="$1"
|
||||
|
||||
if git rev-parse --quiet --verify "refs/tags/$tag" >/dev/null; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
git ls-remote --quiet --exit-code --tags "$push_remote" "refs/tags/$tag" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
copy_patches_to_temp() {
|
||||
local patch_tmp_dir="$1"
|
||||
local patch_file
|
||||
local patch_name
|
||||
local patch_index=0
|
||||
|
||||
mkdir -p "$patch_tmp_dir"
|
||||
for patch_file in "${patch_files[@]}"; do
|
||||
[[ -f "$patch_file" ]] || die "patch file not found: $patch_file"
|
||||
printf -v patch_name '%03d-%s' "$patch_index" "$(basename "$patch_file")"
|
||||
cp "$patch_file" "$patch_tmp_dir/$patch_name"
|
||||
patch_index=$((patch_index + 1))
|
||||
done
|
||||
}
|
||||
|
||||
fetch_upstream_tag() {
|
||||
local tag="$1"
|
||||
|
||||
git fetch --force "$upstream_url" "refs/tags/$tag:refs/tags/$tag"
|
||||
}
|
||||
|
||||
create_release_commit() {
|
||||
local tag="$1"
|
||||
local custom_tag="$2"
|
||||
local branch="$3"
|
||||
local patch_tmp_dir="$4"
|
||||
local patch_file
|
||||
|
||||
git checkout -B "$branch" "$tag"
|
||||
rm -rf "$dist_dir"
|
||||
for patch_file in "$patch_tmp_dir"/*; do
|
||||
[[ -f "$patch_file" ]] || continue
|
||||
log "Applying $(basename "$patch_file")"
|
||||
git apply --3way "$patch_file"
|
||||
done
|
||||
remove_upstream_workflows
|
||||
git add -A
|
||||
git commit -m "apply local Syncthing patches for $tag"
|
||||
git tag -a "$custom_tag" -m "Syncthing $tag with local patches"
|
||||
}
|
||||
|
||||
remove_upstream_workflows() {
|
||||
local workflow_dir
|
||||
|
||||
for workflow_dir in .github/workflows .gitea/workflows; do
|
||||
if [[ -e "$workflow_dir" ]]; then
|
||||
log "Removing upstream workflow directory $workflow_dir from release commit"
|
||||
rm -rf "$workflow_dir"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
delete_local_tag_if_forced() {
|
||||
local tag="$1"
|
||||
|
||||
if [[ "$force" != "1" ]]; then
|
||||
return
|
||||
fi
|
||||
if git rev-parse --quiet --verify "refs/tags/$tag" >/dev/null; then
|
||||
git tag -d "$tag"
|
||||
fi
|
||||
}
|
||||
|
||||
test_release() {
|
||||
if [[ "$run_tests" != "1" ]]; then
|
||||
log "Skipping tests because CUSTOM_RELEASE_TEST=$run_tests"
|
||||
return
|
||||
fi
|
||||
|
||||
rebuild_assets_once
|
||||
go test ./lib/api/auto ./lib/fs ./lib/ignore ./lib/scanner ./lib/model
|
||||
}
|
||||
|
||||
rebuild_assets_once() {
|
||||
if [[ "$assets_rebuilt" == "1" ]]; then
|
||||
return
|
||||
fi
|
||||
|
||||
log "Regenerating embedded GUI assets"
|
||||
go run build.go assets
|
||||
assets_rebuilt=1
|
||||
}
|
||||
|
||||
format_build_specs() {
|
||||
local spec
|
||||
|
||||
for spec in $build_specs; do
|
||||
printf -- '- %s\n' "$spec"
|
||||
done
|
||||
}
|
||||
|
||||
build_release() {
|
||||
local custom_tag="$1"
|
||||
|
||||
rebuild_assets_once
|
||||
rm -rf "$dist_dir"
|
||||
mkdir -p "$dist_dir"
|
||||
|
||||
if [[ -z "$build_specs" ]]; then
|
||||
build_specs="$(go env GOOS)/$(go env GOARCH)/$archive_kind"
|
||||
fi
|
||||
|
||||
local spec
|
||||
for spec in $build_specs; do
|
||||
build_one "$custom_tag" "$spec"
|
||||
done
|
||||
|
||||
cat > "$dist_dir/release-notes.md" <<EOF
|
||||
# $custom_tag
|
||||
|
||||
Syncthing $upstream_tag with the local patches applied.
|
||||
|
||||
Builds:
|
||||
$(format_build_specs)
|
||||
|
||||
Patches:
|
||||
$(printf -- '- %s\n' "${patch_files[@]}")
|
||||
|
||||
Patch behavior:
|
||||
- Root-level .stignore syncs like regular folder content.
|
||||
- .stfolder and .stversions stay protected as Syncthing internals.
|
||||
- The web GUI footer includes "It syncs .stignore now!" as a custom build marker.
|
||||
EOF
|
||||
|
||||
(
|
||||
cd "$dist_dir"
|
||||
if command -v sha256sum >/dev/null 2>&1; then
|
||||
sha256sum ./* > SHA256SUMS
|
||||
else
|
||||
shasum -a 256 ./* > SHA256SUMS
|
||||
fi
|
||||
)
|
||||
}
|
||||
|
||||
build_one() {
|
||||
local custom_tag="$1"
|
||||
local spec="$2"
|
||||
local goos
|
||||
local goarch
|
||||
local kind
|
||||
local cgo_enabled
|
||||
|
||||
IFS=/ read -r goos goarch kind cgo_enabled <<< "$spec"
|
||||
[[ -n "$goos" && -n "$goarch" && -n "$kind" ]] || die "invalid build spec: $spec"
|
||||
cgo_enabled="${cgo_enabled:-$default_cgo_enabled}"
|
||||
|
||||
log "Building $target for $goos/$goarch as $kind with CGO_ENABLED=$cgo_enabled"
|
||||
|
||||
case "$kind" in
|
||||
tar|zip)
|
||||
local archive
|
||||
archive="$(CGO_ENABLED="$cgo_enabled" go run build.go -version "$custom_tag" -goos "$goos" -goarch "$goarch" "$kind" "$target" | tail -n 1)"
|
||||
if [[ "$goos" == "darwin" ]]; then
|
||||
sign_and_validate_darwin_archive "$archive" "$kind"
|
||||
fi
|
||||
mv "$archive" "$dist_dir/"
|
||||
;;
|
||||
binary)
|
||||
CGO_ENABLED="$cgo_enabled" go run build.go -version "$custom_tag" -goos "$goos" -goarch "$goarch" -build-out "$dist_dir/$target-$goos-$goarch" build "$target"
|
||||
if [[ "$goos" == "darwin" ]]; then
|
||||
sign_and_validate_darwin_binary "$dist_dir/$target-$goos-$goarch"
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
die "unknown build archive kind in $spec"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
find_release_binary() {
|
||||
local root="$1"
|
||||
local candidate
|
||||
|
||||
while IFS= read -r candidate; do
|
||||
if [[ -x "$candidate" ]]; then
|
||||
printf '%s\n' "$candidate"
|
||||
return
|
||||
fi
|
||||
done < <(find "$root" -type f -name "$target")
|
||||
|
||||
die "could not find executable $target in $root"
|
||||
}
|
||||
|
||||
sign_and_validate_darwin_archive() {
|
||||
local archive="$1"
|
||||
local kind="$2"
|
||||
local tmp
|
||||
local archive_abs
|
||||
local binary
|
||||
|
||||
[[ -n "$codesign_identity" ]] || die "CUSTOM_RELEASE_CODESIGN_IDENTITY is required for darwin builds"
|
||||
|
||||
tmp="$(mktemp -d)"
|
||||
archive_abs="$(cd "$(dirname "$archive")" && pwd -P)/$(basename "$archive")"
|
||||
case "$kind" in
|
||||
zip)
|
||||
unzip -q "$archive_abs" -d "$tmp"
|
||||
;;
|
||||
tar)
|
||||
tar -xf "$archive_abs" -C "$tmp"
|
||||
;;
|
||||
*)
|
||||
die "cannot sign archive kind $kind"
|
||||
;;
|
||||
esac
|
||||
|
||||
binary="$(find_release_binary "$tmp")"
|
||||
sign_and_validate_darwin_binary "$binary"
|
||||
|
||||
rm -f "$archive_abs"
|
||||
case "$kind" in
|
||||
zip)
|
||||
(
|
||||
cd "$tmp"
|
||||
zip -qr "$archive_abs" .
|
||||
)
|
||||
;;
|
||||
tar)
|
||||
(
|
||||
cd "$tmp"
|
||||
tar -czf "$archive_abs" .
|
||||
)
|
||||
;;
|
||||
esac
|
||||
rm -rf "$tmp"
|
||||
}
|
||||
|
||||
sign_and_validate_darwin_binary() {
|
||||
local binary="$1"
|
||||
local version_output
|
||||
local codesign_details
|
||||
local codesign_args
|
||||
|
||||
[[ -n "$codesign_identity" ]] || die "CUSTOM_RELEASE_CODESIGN_IDENTITY is required for darwin builds"
|
||||
|
||||
codesign_args=(--force --sign "$codesign_identity")
|
||||
if [[ -n "${CUSTOM_RELEASE_KEYCHAIN_PATH:-}" ]]; then
|
||||
if [[ -n "${CUSTOM_RELEASE_KEYCHAIN_PASSWORD:-}" ]]; then
|
||||
security unlock-keychain -p "$CUSTOM_RELEASE_KEYCHAIN_PASSWORD" "$CUSTOM_RELEASE_KEYCHAIN_PATH"
|
||||
fi
|
||||
security find-identity -v -p codesigning "$CUSTOM_RELEASE_KEYCHAIN_PATH"
|
||||
security find-identity -v -p codesigning
|
||||
codesign_args+=(--keychain "$CUSTOM_RELEASE_KEYCHAIN_PATH")
|
||||
fi
|
||||
codesign_args+=(--options runtime --timestamp)
|
||||
codesign "${codesign_args[@]}" "$binary"
|
||||
|
||||
version_output="$("$binary" --version)"
|
||||
if [[ "$version_output" == *modernc-sqlite* ]]; then
|
||||
die "darwin build unexpectedly reports [modernc-sqlite]: $version_output"
|
||||
fi
|
||||
|
||||
codesign --verify --strict --verbose=2 "$binary"
|
||||
codesign_details="$(codesign -dv --verbose=4 "$binary" 2>&1)"
|
||||
if [[ "$codesign_team_id" == "NG5W75WE8U" && "$codesign_details" != *"TeamIdentifier=NG5W75WE8U"* ]]; then
|
||||
printf '%s\n' "$codesign_details" >&2
|
||||
die "darwin build is not signed by TeamIdentifier=NG5W75WE8U"
|
||||
fi
|
||||
if [[ "$codesign_details" != *"TeamIdentifier=$codesign_team_id"* ]]; then
|
||||
printf '%s\n' "$codesign_details" >&2
|
||||
die "darwin build is not signed by TeamIdentifier=$codesign_team_id"
|
||||
fi
|
||||
if [[ "$require_gatekeeper_assessment" == "1" ]]; then
|
||||
spctl -a -vv --type execute "$binary"
|
||||
else
|
||||
log "Skipping Gatekeeper assessment because CUSTOM_RELEASE_REQUIRE_GATEKEEPER_ASSESSMENT=$require_gatekeeper_assessment"
|
||||
fi
|
||||
}
|
||||
|
||||
push_refs() {
|
||||
local branch="$1"
|
||||
local custom_tag="$2"
|
||||
|
||||
if [[ "$push_release" != "1" ]]; then
|
||||
log "Skipping push because CUSTOM_RELEASE_PUSH=$push_release"
|
||||
return
|
||||
fi
|
||||
|
||||
if [[ "$push_branch" == "1" ]]; then
|
||||
git push "$push_remote" "$branch"
|
||||
else
|
||||
log "Skipping release branch push because CUSTOM_RELEASE_PUSH_BRANCH=$push_branch"
|
||||
fi
|
||||
git push "$push_remote" "$custom_tag"
|
||||
}
|
||||
|
||||
publish_release() {
|
||||
local custom_tag="$1"
|
||||
|
||||
if [[ "$publish_gitea_release" != "1" ]]; then
|
||||
log "Skipping Gitea release publishing because CUSTOM_RELEASE_CREATE_GITEA_RELEASE=$publish_gitea_release"
|
||||
return
|
||||
fi
|
||||
|
||||
command -v tea >/dev/null 2>&1 || die "tea is required for Gitea release publishing"
|
||||
|
||||
local args=(releases create "$custom_tag" --title "$custom_tag" --note-file "$dist_dir/release-notes.md")
|
||||
if [[ -n "$tea_repo" ]]; then
|
||||
args+=(--repo "$tea_repo")
|
||||
else
|
||||
args+=(--remote "$push_remote")
|
||||
fi
|
||||
|
||||
local asset
|
||||
for asset in "$dist_dir"/*; do
|
||||
[[ -f "$asset" ]] || continue
|
||||
[[ "$(basename "$asset")" == "release-notes.md" ]] && continue
|
||||
args+=(--asset "$asset")
|
||||
done
|
||||
|
||||
tea "${args[@]}"
|
||||
}
|
||||
|
||||
main() {
|
||||
require_clean_worktree
|
||||
|
||||
if [[ -z "$upstream_tag" ]]; then
|
||||
upstream_tag="$(latest_stable_tag)"
|
||||
fi
|
||||
[[ -n "$upstream_tag" ]] || die "could not determine latest upstream tag"
|
||||
|
||||
local custom_tag="${upstream_tag}-${suffix}"
|
||||
local branch="${branch_prefix}/${upstream_tag#v}-${suffix}"
|
||||
|
||||
if tag_exists "$custom_tag" && [[ "$force" != "1" ]]; then
|
||||
log "Custom release $custom_tag already exists; nothing to do."
|
||||
return
|
||||
fi
|
||||
|
||||
patch_tmp_dir="$(mktemp -d)"
|
||||
trap 'rm -rf "$patch_tmp_dir"' EXIT
|
||||
|
||||
copy_patches_to_temp "$patch_tmp_dir"
|
||||
fetch_upstream_tag "$upstream_tag"
|
||||
delete_local_tag_if_forced "$custom_tag"
|
||||
create_release_commit "$upstream_tag" "$custom_tag" "$branch" "$patch_tmp_dir"
|
||||
test_release
|
||||
build_release "$custom_tag"
|
||||
push_refs "$branch" "$custom_tag"
|
||||
publish_release "$custom_tag"
|
||||
|
||||
log "Built custom release $custom_tag from upstream $upstream_tag"
|
||||
log "Artifacts are in $dist_dir/"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
Reference in New Issue
Block a user