diff --git a/update/check.go b/update/check.go index 56116459..75b551bb 100644 --- a/update/check.go +++ b/update/check.go @@ -1,13 +1,101 @@ package update +import ( + "encoding/json" + "fmt" + "io/ioutil" + "time" +) + +const versionCheckPeriod = 8 * 60 * 60 + +// VersionInfo - VersionInfo type VersionInfo struct { NewVersion string Announcement string AnnouncementURL string SelfUpdateMinVersion string CanAutoUpdate bool + PackageURL string } -func (u *Updater) GetVersionResponse() (VersionInfo, error) { - return VersionInfo{}, nil +// GetVersionResponse - GetVersionResponse +func (u *Updater) GetVersionResponse(forceRecheck bool) (VersionInfo, error) { + if !forceRecheck && + u.versionCheckLastTime.Unix()+versionCheckPeriod > time.Now().Unix() { + return u.parseVersionResponse(u.versionJSON) + } + + resp, err := u.Client.Get(u.VersionURL) + if resp != nil && resp.Body != nil { + defer resp.Body.Close() + } + + if err != nil { + return VersionInfo{}, fmt.Errorf("updater: HTTP GET %s: %s", u.VersionURL, err) + } + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return VersionInfo{}, fmt.Errorf("updater: HTTP GET %s: %s", u.VersionURL, err) + } + + u.versionJSON = body + u.versionCheckLastTime = time.Now() + + return u.parseVersionResponse(body) +} + +func (u *Updater) parseVersionResponse(data []byte) (VersionInfo, error) { + info := VersionInfo{} + versionJSON := make(map[string]interface{}) + err := json.Unmarshal(data, &versionJSON) + if err != nil { + return info, fmt.Errorf("version.json: %s", err) + } + + var ok1, ok2, ok3, ok4 bool + info.NewVersion, ok1 = versionJSON["version"].(string) + info.Announcement, ok2 = versionJSON["announcement"].(string) + info.AnnouncementURL, ok3 = versionJSON["announcement_url"].(string) + info.SelfUpdateMinVersion, ok4 = versionJSON["selfupdate_min_version"].(string) + if !ok1 || !ok2 || !ok3 || !ok4 { + return info, fmt.Errorf("version.json: invalid data") + } + + var ok bool + info.PackageURL, ok = u.getDownloadURL(versionJSON) + if ok && + info.NewVersion != u.VersionString && + u.VersionString >= info.SelfUpdateMinVersion { + info.CanAutoUpdate = true + } + + return info, nil +} + +// Get download URL for the current GOOS/GOARCH/ARMVersion +func (u *Updater) getDownloadURL(json map[string]interface{}) (string, bool) { + var key string + + if u.Arch == "arm" && u.ARMVersion != "" { + // the key is: + // download_linux_armv5 for ARMv5 + // download_linux_armv6 for ARMv6 + // download_linux_armv7 for ARMv7 + key = fmt.Sprintf("download_%s_%sv%s", u.OS, u.Arch, u.ARMVersion) + } + + val, ok := json[key] + if !ok { + // the key is download_linux_arm or download_linux_arm64 for regular ARM versions + key = fmt.Sprintf("download_%s_%s", u.OS, u.Arch) + val, ok = json[key] + } + + if !ok { + return "", false + } + + return val.(string), true } diff --git a/update/test/AdGuardHome.tar.gz b/update/test/AdGuardHome.tar.gz new file mode 100644 index 00000000..b292c6d9 Binary files /dev/null and b/update/test/AdGuardHome.tar.gz differ diff --git a/update/test/AdGuardHome.zip b/update/test/AdGuardHome.zip new file mode 100644 index 00000000..c984a347 Binary files /dev/null and b/update/test/AdGuardHome.zip differ diff --git a/update/update_test.go b/update/update_test.go new file mode 100644 index 00000000..6a9261df --- /dev/null +++ b/update/update_test.go @@ -0,0 +1,205 @@ +package update + +import ( + "fmt" + "io/ioutil" + "net" + "net/http" + "os" + "testing" + + "github.com/stretchr/testify/assert" +) + +func startHTTPServer(data string) (net.Listener, uint16) { + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + _, _ = w.Write([]byte(data)) + }) + + listener, err := net.Listen("tcp", ":0") + if err != nil { + panic(err) + } + + go func() { _ = http.Serve(listener, nil) }() + return listener, uint16(listener.Addr().(*net.TCPAddr).Port) +} + +func TestUpdateGetVersion(t *testing.T) { + const jsonData = `{ + "version": "v0.103.0-beta2", + "announcement": "AdGuard Home v0.103.0-beta2 is now available!", + "announcement_url": "https://github.com/AdguardTeam/AdGuardHome/releases", + "selfupdate_min_version": "v0.0", + "download_windows_amd64": "https://static.adguard.com/adguardhome/beta/AdGuardHome_windows_amd64.zip", + "download_windows_386": "https://static.adguard.com/adguardhome/beta/AdGuardHome_windows_386.zip", + "download_darwin_amd64": "https://static.adguard.com/adguardhome/beta/AdGuardHome_darwin_amd64.zip", + "download_darwin_386": "https://static.adguard.com/adguardhome/beta/AdGuardHome_darwin_386.zip", + "download_linux_amd64": "https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_amd64.tar.gz", + "download_linux_386": "https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_386.tar.gz", + "download_linux_arm": "https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_armv6.tar.gz", + "download_linux_armv5": "https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_armv5.tar.gz", + "download_linux_armv6": "https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_armv6.tar.gz", + "download_linux_armv7": "https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_armv7.tar.gz", + "download_linux_arm64": "https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_arm64.tar.gz", + "download_linux_mips": "https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_mips_softfloat.tar.gz", + "download_linux_mipsle": "https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_mipsle_softfloat.tar.gz", + "download_linux_mips64": "https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_mips64_softfloat.tar.gz", + "download_linux_mips64le": "https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_mips64le_softfloat.tar.gz", + "download_freebsd_386": "https://static.adguard.com/adguardhome/beta/AdGuardHome_freebsd_386.tar.gz", + "download_freebsd_amd64": "https://static.adguard.com/adguardhome/beta/AdGuardHome_freebsd_amd64.tar.gz", + "download_freebsd_arm": "https://static.adguard.com/adguardhome/beta/AdGuardHome_freebsd_armv6.tar.gz", + "download_freebsd_armv5": "https://static.adguard.com/adguardhome/beta/AdGuardHome_freebsd_armv5.tar.gz", + "download_freebsd_armv6": "https://static.adguard.com/adguardhome/beta/AdGuardHome_freebsd_armv6.tar.gz", + "download_freebsd_armv7": "https://static.adguard.com/adguardhome/beta/AdGuardHome_freebsd_armv7.tar.gz", + "download_freebsd_arm64": "https://static.adguard.com/adguardhome/beta/AdGuardHome_freebsd_arm64.tar.gz" +}` + + l, lport := startHTTPServer(jsonData) + defer func() { _ = l.Close() }() + + u := NewUpdater("") + u.Client = &http.Client{} + u.VersionURL = fmt.Sprintf("http://127.0.0.1:%d/", lport) + u.OS = "linux" + u.Arch = "arm" + u.VersionString = "v0.103.0-beta1" + + info, err := u.GetVersionResponse(false) + assert.Nil(t, err) + assert.Equal(t, "v0.103.0-beta2", info.NewVersion) + assert.Equal(t, "AdGuard Home v0.103.0-beta2 is now available!", info.Announcement) + assert.Equal(t, "https://github.com/AdguardTeam/AdGuardHome/releases", info.AnnouncementURL) + assert.Equal(t, "v0.0", info.SelfUpdateMinVersion) + assert.True(t, info.CanAutoUpdate) + assert.Equal(t, "https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_armv6.tar.gz", info.PackageURL) + + _ = l.Close() + + // check cached + _, err = u.GetVersionResponse(false) + assert.Nil(t, err) +} + +func TestUpdate(t *testing.T) { + _ = os.Mkdir("aghtest", 0755) + defer func() { + _ = os.RemoveAll("aghtest") + }() + + // create "current" files + assert.Nil(t, ioutil.WriteFile("aghtest/AdGuardHome", []byte("AdGuardHome"), 0755)) + assert.Nil(t, ioutil.WriteFile("aghtest/README.md", []byte("README.md"), 0644)) + assert.Nil(t, ioutil.WriteFile("aghtest/LICENSE.txt", []byte("LICENSE.txt"), 0644)) + assert.Nil(t, ioutil.WriteFile("aghtest/AdGuardHome.yaml", []byte("AdGuardHome.yaml"), 0644)) + + // start server for returning package file + pkgData, err := ioutil.ReadFile("test/AdGuardHome.tar.gz") + assert.Nil(t, err) + l, lport := startHTTPServer(string(pkgData)) + defer func() { _ = l.Close() }() + + u := NewUpdater("aghtest") + u.Client = &http.Client{} + u.PackageURL = fmt.Sprintf("http://127.0.0.1:%d/AdGuardHome.tar.gz", lport) + u.VersionString = "v0.103.0" + u.NewVersion = "v0.103.1" + u.ConfigName = "aghtest/AdGuardHome.yaml" + + assert.Nil(t, u.prepare()) + u.currentExeName = "aghtest/AdGuardHome" + assert.Nil(t, u.downloadPackageFile(u.PackageURL, u.packageName)) + assert.Nil(t, u.unpack()) + // assert.Nil(t, u.check()) + assert.Nil(t, u.backup()) + assert.Nil(t, u.replace()) + u.clean() + + // check backup files + d, err := ioutil.ReadFile("aghtest/agh-backup/AdGuardHome.yaml") + assert.Nil(t, err) + assert.Equal(t, "AdGuardHome.yaml", string(d)) + + d, err = ioutil.ReadFile("aghtest/agh-backup/AdGuardHome") + assert.Nil(t, err) + assert.Equal(t, "AdGuardHome", string(d)) + + // check updated files + d, err = ioutil.ReadFile("aghtest/AdGuardHome") + assert.Nil(t, err) + assert.Equal(t, "1", string(d)) + + d, err = ioutil.ReadFile("aghtest/README.md") + assert.Nil(t, err) + assert.Equal(t, "2", string(d)) + + d, err = ioutil.ReadFile("aghtest/LICENSE.txt") + assert.Nil(t, err) + assert.Equal(t, "3", string(d)) + + d, err = ioutil.ReadFile("aghtest/AdGuardHome.yaml") + assert.Nil(t, err) + assert.Equal(t, "AdGuardHome.yaml", string(d)) +} + +func TestUpdateWindows(t *testing.T) { + _ = os.Mkdir("aghtest", 0755) + defer func() { + _ = os.RemoveAll("aghtest") + }() + + // create "current" files + assert.Nil(t, ioutil.WriteFile("aghtest/AdGuardHome.exe", []byte("AdGuardHome.exe"), 0755)) + assert.Nil(t, ioutil.WriteFile("aghtest/README.md", []byte("README.md"), 0644)) + assert.Nil(t, ioutil.WriteFile("aghtest/LICENSE.txt", []byte("LICENSE.txt"), 0644)) + assert.Nil(t, ioutil.WriteFile("aghtest/AdGuardHome.yaml", []byte("AdGuardHome.yaml"), 0644)) + + // start server for returning package file + pkgData, err := ioutil.ReadFile("test/AdGuardHome.zip") + assert.Nil(t, err) + l, lport := startHTTPServer(string(pkgData)) + defer func() { _ = l.Close() }() + + u := NewUpdater("aghtest") + u.Client = &http.Client{} + u.PackageURL = fmt.Sprintf("http://127.0.0.1:%d/AdGuardHome.zip", lport) + u.OS = "windows" + u.VersionString = "v0.103.0" + u.NewVersion = "v0.103.1" + u.ConfigName = "aghtest/AdGuardHome.yaml" + + assert.Nil(t, u.prepare()) + u.currentExeName = "aghtest/AdGuardHome.exe" + assert.Nil(t, u.downloadPackageFile(u.PackageURL, u.packageName)) + assert.Nil(t, u.unpack()) + // assert.Nil(t, u.check()) + assert.Nil(t, u.backup()) + assert.Nil(t, u.replace()) + u.clean() + + // check backup files + d, err := ioutil.ReadFile("aghtest/agh-backup/AdGuardHome.yaml") + assert.Nil(t, err) + assert.Equal(t, "AdGuardHome.yaml", string(d)) + + d, err = ioutil.ReadFile("aghtest/agh-backup/AdGuardHome.exe") + assert.Nil(t, err) + assert.Equal(t, "AdGuardHome.exe", string(d)) + + // check updated files + d, err = ioutil.ReadFile("aghtest/AdGuardHome.exe") + assert.Nil(t, err) + assert.Equal(t, "1", string(d)) + + d, err = ioutil.ReadFile("aghtest/README.md") + assert.Nil(t, err) + assert.Equal(t, "2", string(d)) + + d, err = ioutil.ReadFile("aghtest/LICENSE.txt") + assert.Nil(t, err) + assert.Equal(t, "3", string(d)) + + d, err = ioutil.ReadFile("aghtest/AdGuardHome.yaml") + assert.Nil(t, err) + assert.Equal(t, "AdGuardHome.yaml", string(d)) +} diff --git a/update/updater.go b/update/updater.go index 32a092ca..cb094f79 100644 --- a/update/updater.go +++ b/update/updater.go @@ -1,30 +1,54 @@ package update import ( + "archive/tar" + "archive/zip" + "compress/gzip" + "fmt" + "io" + "io/ioutil" + "net/http" "os" + "os/exec" "path/filepath" + "strings" "time" + + "github.com/AdguardTeam/golibs/log" ) +// Updater - Updater type Updater struct { - DisableUpdate bool + Client *http.Client + VersionURL string // version.json URL + VersionString string + OS string // GOOS + Arch string // GOARCH + ARMVersion string // ARM version, e.g. "6" + NewVersion string // VersionInfo.NewVersion + PackageURL string // VersionInfo.PackageURL + ConfigName string // current config file ".../AdGuardHome.yaml" - currentBinary string // current binary executable - workDir string // updater work dir (where backup/upd dirs will be created) + currentExeName string // current binary executable + workDir string // updater work dir (where backup/upd dirs will be created) + updateDir string // "work_dir/agh-update-v0.103.0" + packageName string // "work_dir/agh-update-v0.103.0/pkg_name.tar.gz" + backupDir string // "work_dir/agh-backup" + backupExeName string // "work_dir/agh-backup/AdGuardHome[.exe]" + updateExeName string // "work_dir/agh-update-v0.103.0/AdGuardHome[.exe]" + unpackedFiles []string // cached version.json to avoid hammering github.io for each page reload - versionCheckJSON []byte + versionJSON []byte versionCheckLastTime time.Time } // NewUpdater - creates a new instance of the Updater func NewUpdater(workDir string) *Updater { - return &Updater{ - currentBinary: filepath.Base(os.Args[0]), - workDir: workDir, - versionCheckJSON: nil, - versionCheckLastTime: time.Time{}, + u := &Updater{ + workDir: workDir, } + return u } // DoUpdate - conducts the auto-update @@ -32,7 +56,347 @@ func NewUpdater(workDir string) *Updater { // 2. Unpacks it and checks the contents // 3. Backups the current version and configuration // 4. Replaces the old files -// 5. Restarts the service func (u *Updater) DoUpdate() error { + err := u.prepare() + if err != nil { + return err + } + + defer u.clean() + + err = u.downloadPackageFile(u.PackageURL, u.packageName) + if err != nil { + return err + } + + err = u.unpack() + if err != nil { + return err + } + + err = u.check() + if err != nil { + u.clean() + return err + } + + err = u.backup() + if err != nil { + return err + } + + err = u.replace() + if err != nil { + return err + } + + return nil +} + +func (u *Updater) prepare() error { + u.updateDir = filepath.Join(u.workDir, fmt.Sprintf("agh-update-%s", u.NewVersion)) + + _, pkgNameOnly := filepath.Split(u.PackageURL) + if len(pkgNameOnly) == 0 { + return fmt.Errorf("invalid PackageURL") + } + u.packageName = filepath.Join(u.updateDir, pkgNameOnly) + u.backupDir = filepath.Join(u.workDir, "agh-backup") + + exeName := "AdGuardHome" + if u.OS == "windows" { + exeName = "AdGuardHome.exe" + } + + u.backupExeName = filepath.Join(u.backupDir, exeName) + u.updateExeName = filepath.Join(u.updateDir, exeName) + u.currentExeName = os.Args[0] + return nil +} + +func (u *Updater) unpack() error { + var err error + _, pkgNameOnly := filepath.Split(u.PackageURL) + + log.Debug("updater: unpacking the package") + if strings.HasSuffix(pkgNameOnly, ".zip") { + u.unpackedFiles, err = zipFileUnpack(u.packageName, u.updateDir) + if err != nil { + return fmt.Errorf(".zip unpack failed: %s", err) + } + + } else if strings.HasSuffix(pkgNameOnly, ".tar.gz") { + u.unpackedFiles, err = tarGzFileUnpack(u.packageName, u.updateDir) + if err != nil { + return fmt.Errorf(".tar.gz unpack failed: %s", err) + } + + } else { + return fmt.Errorf("unknown package extension") + } + + return nil +} + +func (u *Updater) check() error { + log.Debug("updater: checking configuration") + err := copyFile(u.ConfigName, filepath.Join(u.updateDir, "AdGuardHome.yaml")) + if err != nil { + return fmt.Errorf("copyFile() failed: %s", err) + } + cmd := exec.Command(u.updateExeName, "--check-config") + err = cmd.Run() + if err != nil || cmd.ProcessState.ExitCode() != 0 { + return fmt.Errorf("exec.Command(): %s %d", err, cmd.ProcessState.ExitCode()) + } + return nil +} + +func (u *Updater) backup() error { + log.Debug("updater: backing up the current configuration") + _ = os.Mkdir(u.backupDir, 0755) + err := copyFile(u.ConfigName, filepath.Join(u.backupDir, "AdGuardHome.yaml")) + if err != nil { + return fmt.Errorf("copyFile() failed: %s", err) + } + + // workdir/README.md -> backup/README.md + err = copySupportingFiles(u.unpackedFiles, u.workDir, u.backupDir) + if err != nil { + return fmt.Errorf("copySupportingFiles(%s, %s) failed: %s", + u.workDir, u.backupDir, err) + } + + return nil +} + +func (u *Updater) replace() error { + // update/README.md -> workdir/README.md + err := copySupportingFiles(u.unpackedFiles, u.updateDir, u.workDir) + if err != nil { + return fmt.Errorf("copySupportingFiles(%s, %s) failed: %s", + u.updateDir, u.workDir, err) + } + + log.Debug("updater: renaming: %s -> %s", u.currentExeName, u.backupExeName) + err = os.Rename(u.currentExeName, u.backupExeName) + if err != nil { + return err + } + + if u.OS == "windows" { + // rename fails with "File in use" error + err = copyFile(u.updateExeName, u.currentExeName) + } else { + err = os.Rename(u.updateExeName, u.currentExeName) + } + if err != nil { + return err + } + log.Debug("updater: renamed: %s -> %s", u.updateExeName, u.currentExeName) + return nil +} + +func (u *Updater) clean() { + _ = os.RemoveAll(u.updateDir) +} + +// Download package file and save it to disk +func (u *Updater) downloadPackageFile(url string, filename string) error { + resp, err := u.Client.Get(url) + if err != nil { + return fmt.Errorf("HTTP request failed: %s", err) + } + if resp != nil && resp.Body != nil { + defer resp.Body.Close() + } + + log.Debug("updater: reading HTTP body") + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return fmt.Errorf("ioutil.ReadAll() failed: %s", err) + } + + _ = os.Mkdir(u.updateDir, 0755) + + log.Debug("updater: saving package to file") + err = ioutil.WriteFile(filename, body, 0644) + if err != nil { + return fmt.Errorf("ioutil.WriteFile() failed: %s", err) + } + return nil +} + +// Unpack all files from .tar.gz file to the specified directory +// Existing files are overwritten +// All files are created inside 'outdir', subdirectories are not created +// Return the list of files (not directories) written +func tarGzFileUnpack(tarfile, outdir string) ([]string, error) { + f, err := os.Open(tarfile) + if err != nil { + return nil, fmt.Errorf("os.Open(): %s", err) + } + defer func() { + _ = f.Close() + }() + + gzReader, err := gzip.NewReader(f) + if err != nil { + return nil, fmt.Errorf("gzip.NewReader(): %s", err) + } + + var files []string + var err2 error + tarReader := tar.NewReader(gzReader) + for { + header, err := tarReader.Next() + if err == io.EOF { + err2 = nil + break + } + if err != nil { + err2 = fmt.Errorf("tarReader.Next(): %s", err) + break + } + + _, inputNameOnly := filepath.Split(header.Name) + if len(inputNameOnly) == 0 { + continue + } + + outputName := filepath.Join(outdir, inputNameOnly) + + if header.Typeflag == tar.TypeDir { + err = os.Mkdir(outputName, os.FileMode(header.Mode&0777)) + if err != nil && !os.IsExist(err) { + err2 = fmt.Errorf("os.Mkdir(%s): %s", outputName, err) + break + } + log.Debug("updater: created directory %s", outputName) + continue + } else if header.Typeflag != tar.TypeReg { + log.Debug("updater: %s: unknown file type %d, skipping", inputNameOnly, header.Typeflag) + continue + } + + f, err := os.OpenFile(outputName, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, os.FileMode(header.Mode&0777)) + if err != nil { + err2 = fmt.Errorf("os.OpenFile(%s): %s", outputName, err) + break + } + _, err = io.Copy(f, tarReader) + if err != nil { + _ = f.Close() + err2 = fmt.Errorf("io.Copy(): %s", err) + break + } + err = f.Close() + if err != nil { + err2 = fmt.Errorf("f.Close(): %s", err) + break + } + + log.Debug("updater: created file %s", outputName) + files = append(files, header.Name) + } + + _ = gzReader.Close() + return files, err2 +} + +// Unpack all files from .zip file to the specified directory +// Existing files are overwritten +// All files are created inside 'outdir', subdirectories are not created +// Return the list of files (not directories) written +func zipFileUnpack(zipfile, outdir string) ([]string, error) { + r, err := zip.OpenReader(zipfile) + if err != nil { + return nil, fmt.Errorf("zip.OpenReader(): %s", err) + } + defer r.Close() + + var files []string + var err2 error + var zr io.ReadCloser + for _, zf := range r.File { + zr, err = zf.Open() + if err != nil { + err2 = fmt.Errorf("zip file Open(): %s", err) + break + } + + fi := zf.FileInfo() + inputNameOnly := fi.Name() + if len(inputNameOnly) == 0 { + continue + } + + outputName := filepath.Join(outdir, inputNameOnly) + + if fi.IsDir() { + err = os.Mkdir(outputName, fi.Mode()) + if err != nil && !os.IsExist(err) { + err2 = fmt.Errorf("os.Mkdir(): %s", err) + break + } + log.Tracef("created directory %s", outputName) + continue + } + + f, err := os.OpenFile(outputName, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, fi.Mode()) + if err != nil { + err2 = fmt.Errorf("os.OpenFile(): %s", err) + break + } + _, err = io.Copy(f, zr) + if err != nil { + _ = f.Close() + err2 = fmt.Errorf("io.Copy(): %s", err) + break + } + err = f.Close() + if err != nil { + err2 = fmt.Errorf("f.Close(): %s", err) + break + } + + log.Tracef("created file %s", outputName) + files = append(files, inputNameOnly) + } + + _ = zr.Close() + return files, err2 +} + +// Copy file on disk +func copyFile(src, dst string) error { + d, e := ioutil.ReadFile(src) + if e != nil { + return e + } + e = ioutil.WriteFile(dst, d, 0644) + if e != nil { + return e + } + return nil +} + +func copySupportingFiles(files []string, srcdir, dstdir string) error { + for _, f := range files { + _, name := filepath.Split(f) + if name == "AdGuardHome" || name == "AdGuardHome.exe" || name == "AdGuardHome.yaml" { + continue + } + + src := filepath.Join(srcdir, name) + dst := filepath.Join(dstdir, name) + + err := copyFile(src, dst) + if err != nil && !os.IsNotExist(err) { + return err + } + + log.Debug("updater: copied: %s -> %s", src, dst) + } return nil }