Establish beta and staging release channels, move indexes to helper
This commit is contained in:
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
|
|
||||||
"github.com/safing/portbase/notifications"
|
"github.com/safing/portbase/notifications"
|
||||||
|
"github.com/safing/portmaster/updates/helper"
|
||||||
"github.com/tevino/abool"
|
"github.com/tevino/abool"
|
||||||
|
|
||||||
"github.com/safing/portbase/config"
|
"github.com/safing/portbase/config"
|
||||||
@@ -20,6 +21,7 @@ var (
|
|||||||
devMode config.BoolOption
|
devMode config.BoolOption
|
||||||
enableUpdates config.BoolOption
|
enableUpdates config.BoolOption
|
||||||
|
|
||||||
|
initialReleaseChannel string
|
||||||
previousReleaseChannel string
|
previousReleaseChannel string
|
||||||
updatesCurrentlyEnabled bool
|
updatesCurrentlyEnabled bool
|
||||||
previousDevMode bool
|
previousDevMode bool
|
||||||
@@ -29,21 +31,28 @@ var (
|
|||||||
func registerConfig() error {
|
func registerConfig() error {
|
||||||
err := config.Register(&config.Option{
|
err := config.Register(&config.Option{
|
||||||
Name: "Release Channel",
|
Name: "Release Channel",
|
||||||
Key: releaseChannelKey,
|
Key: helper.ReleaseChannelKey,
|
||||||
Description: "Switch release channel.",
|
Description: "Switch release channel.",
|
||||||
OptType: config.OptTypeString,
|
OptType: config.OptTypeString,
|
||||||
ExpertiseLevel: config.ExpertiseLevelDeveloper,
|
ExpertiseLevel: config.ExpertiseLevelDeveloper,
|
||||||
ReleaseLevel: config.ReleaseLevelExperimental,
|
ReleaseLevel: config.ReleaseLevelExperimental,
|
||||||
RequiresRestart: false,
|
RequiresRestart: true,
|
||||||
DefaultValue: releaseChannelStable,
|
DefaultValue: helper.ReleaseChannelStable,
|
||||||
PossibleValues: []config.PossibleValue{
|
PossibleValues: []config.PossibleValue{
|
||||||
{
|
{
|
||||||
Name: "Stable",
|
Name: "Stable",
|
||||||
Value: releaseChannelStable,
|
Description: "Production releases.",
|
||||||
|
Value: helper.ReleaseChannelStable,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "Beta",
|
Name: "Beta",
|
||||||
Value: releaseChannelBeta,
|
Description: "Production releases for testing new features that may break and cause interruption.",
|
||||||
|
Value: helper.ReleaseChannelBeta,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Staging",
|
||||||
|
Description: "Development releases for testing random things and experimenting. Dangerous - only use when told so.",
|
||||||
|
Value: helper.ReleaseChannelStaging,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Annotations: config.Annotations{
|
Annotations: config.Annotations{
|
||||||
@@ -78,7 +87,8 @@ func registerConfig() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func initConfig() {
|
func initConfig() {
|
||||||
releaseChannel = config.GetAsString(releaseChannelKey, releaseChannelStable)
|
releaseChannel = config.GetAsString(helper.ReleaseChannelKey, helper.ReleaseChannelStable)
|
||||||
|
initialReleaseChannel = releaseChannel()
|
||||||
previousReleaseChannel = releaseChannel()
|
previousReleaseChannel = releaseChannel()
|
||||||
|
|
||||||
enableUpdates = config.GetAsBool(enableUpdatesKey, true)
|
enableUpdates = config.GetAsBool(enableUpdatesKey, true)
|
||||||
@@ -107,7 +117,7 @@ func updateRegistryConfig(_ context.Context, _ interface{}) error {
|
|||||||
changed := false
|
changed := false
|
||||||
|
|
||||||
if releaseChannel() != previousReleaseChannel {
|
if releaseChannel() != previousReleaseChannel {
|
||||||
registry.SetBeta(releaseChannel() == releaseChannelBeta)
|
registry.SetUsePreReleases(releaseChannel() != helper.ReleaseChannelStable)
|
||||||
previousReleaseChannel = releaseChannel()
|
previousReleaseChannel = releaseChannel()
|
||||||
changed = true
|
changed = true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,14 +11,14 @@ import (
|
|||||||
"github.com/safing/portbase/info"
|
"github.com/safing/portbase/info"
|
||||||
"github.com/safing/portbase/log"
|
"github.com/safing/portbase/log"
|
||||||
"github.com/safing/portbase/updater"
|
"github.com/safing/portbase/updater"
|
||||||
|
"github.com/safing/portmaster/updates/helper"
|
||||||
)
|
)
|
||||||
|
|
||||||
// database key for update information
|
// Database key for update information.
|
||||||
const (
|
const (
|
||||||
versionsDBKey = "core:status/versions"
|
versionsDBKey = "core:status/versions"
|
||||||
)
|
)
|
||||||
|
|
||||||
// working vars
|
|
||||||
var (
|
var (
|
||||||
versionExport *versions
|
versionExport *versions
|
||||||
versionExportDB = database.NewInterface(&database.Options{
|
versionExportDB = database.NewInterface(&database.Options{
|
||||||
@@ -35,6 +35,7 @@ type versions struct {
|
|||||||
|
|
||||||
Core *info.Info
|
Core *info.Info
|
||||||
Resources map[string]*updater.Resource
|
Resources map[string]*updater.Resource
|
||||||
|
Channel string
|
||||||
Beta bool
|
Beta bool
|
||||||
Staging bool
|
Staging bool
|
||||||
|
|
||||||
@@ -45,8 +46,9 @@ func initVersionExport() (err error) {
|
|||||||
// init export struct
|
// init export struct
|
||||||
versionExport = &versions{
|
versionExport = &versions{
|
||||||
internalSave: true,
|
internalSave: true,
|
||||||
Beta: registry.Beta,
|
Channel: initialReleaseChannel,
|
||||||
Staging: staging,
|
Beta: initialReleaseChannel == helper.ReleaseChannelBeta,
|
||||||
|
Staging: initialReleaseChannel == helper.ReleaseChannelStaging,
|
||||||
}
|
}
|
||||||
versionExport.SetKey(versionsDBKey)
|
versionExport.SetKey(versionsDBKey)
|
||||||
|
|
||||||
@@ -68,7 +70,7 @@ func stopVersionExport() error {
|
|||||||
return versionExportHook.Cancel()
|
return versionExportHook.Cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
// export is an event hook
|
// export is an event hook.
|
||||||
func export(_ context.Context, _ interface{}) error {
|
func export(_ context.Context, _ interface{}) error {
|
||||||
// populate
|
// populate
|
||||||
versionExport.lock.Lock()
|
versionExport.lock.Lock()
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package helper
|
package helper
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@@ -54,14 +53,6 @@ func EnsureChromeSandboxPermissions(reg *updater.ResourceRegistry) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// PlatformIdentifier converts identifier for the current platform.
|
|
||||||
func PlatformIdentifier(identifier string) string {
|
|
||||||
// From https://golang.org/pkg/runtime/#GOARCH
|
|
||||||
// GOOS is the running program's operating system target: one of darwin, freebsd, linux, and so on.
|
|
||||||
// GOARCH is the running program's architecture target: one of 386, amd64, arm, s390x, and so on.
|
|
||||||
return fmt.Sprintf("%s_%s/%s", runtime.GOOS, runtime.GOARCH, identifier)
|
|
||||||
}
|
|
||||||
|
|
||||||
func checkSysctl(setting string, value byte) bool {
|
func checkSysctl(setting string, value byte) bool {
|
||||||
c, err := sysctl(setting)
|
c, err := sysctl(setting)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
50
updates/helper/indexes.go
Normal file
50
updates/helper/indexes.go
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
package helper
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/safing/portbase/updater"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
ReleaseChannelKey = "core/releaseChannel"
|
||||||
|
ReleaseChannelJSONKey = "core.releaseChannel"
|
||||||
|
ReleaseChannelStable = "stable"
|
||||||
|
ReleaseChannelBeta = "beta"
|
||||||
|
ReleaseChannelStaging = "staging"
|
||||||
|
)
|
||||||
|
|
||||||
|
func SetIndexes(registry *updater.ResourceRegistry, releaseChannel string) {
|
||||||
|
// Be reminded that the order is important, as indexes added later will
|
||||||
|
// override the current release from earlier indexes.
|
||||||
|
|
||||||
|
// Reset indexes before adding them (again).
|
||||||
|
registry.ResetIndexes()
|
||||||
|
|
||||||
|
// Always add the stable index as a base.
|
||||||
|
registry.AddIndex(updater.Index{
|
||||||
|
Path: "stable.json",
|
||||||
|
})
|
||||||
|
|
||||||
|
// Add beta index if in beta or staging channel.
|
||||||
|
if releaseChannel == ReleaseChannelBeta ||
|
||||||
|
releaseChannel == ReleaseChannelStaging {
|
||||||
|
registry.AddIndex(updater.Index{
|
||||||
|
Path: "beta.json",
|
||||||
|
PreRelease: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add staging index if in staging channel.
|
||||||
|
if releaseChannel == ReleaseChannelStaging {
|
||||||
|
registry.AddIndex(updater.Index{
|
||||||
|
Path: "staging.json",
|
||||||
|
PreRelease: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the intel index last, as it updates the fastest and should not be
|
||||||
|
// crippled by other faulty indexes. It can only specify versions for its
|
||||||
|
// scope anyway.
|
||||||
|
registry.AddIndex(updater.Index{
|
||||||
|
Path: "all/intel/intel.json",
|
||||||
|
})
|
||||||
|
}
|
||||||
68
updates/helper/updates.go
Normal file
68
updates/helper/updates.go
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
package helper
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"runtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
onWindows = runtime.GOOS == "windows"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PlatformIdentifier converts identifier for the current platform.
|
||||||
|
func PlatformIdentifier(identifier string) string {
|
||||||
|
// From https://golang.org/pkg/runtime/#GOARCH
|
||||||
|
// GOOS is the running program's operating system target: one of darwin, freebsd, linux, and so on.
|
||||||
|
// GOARCH is the running program's architecture target: one of 386, amd64, arm, s390x, and so on.
|
||||||
|
return fmt.Sprintf("%s_%s/%s", runtime.GOOS, runtime.GOARCH, identifier)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MandatoryUpdates returns mandatory updates that should be loaded on install
|
||||||
|
// or reset.
|
||||||
|
func MandatoryUpdates() (identifiers []string) {
|
||||||
|
// Binaries
|
||||||
|
if onWindows {
|
||||||
|
identifiers = []string{
|
||||||
|
PlatformIdentifier("core/portmaster-core.exe"),
|
||||||
|
PlatformIdentifier("kext/portmaster-kext.dll"),
|
||||||
|
PlatformIdentifier("kext/portmaster-kext.sys"),
|
||||||
|
PlatformIdentifier("start/portmaster-start.exe"),
|
||||||
|
PlatformIdentifier("notifier/portmaster-notifier.exe"),
|
||||||
|
PlatformIdentifier("notifier/portmaster-snoretoast.exe"),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
identifiers = []string{
|
||||||
|
PlatformIdentifier("core/portmaster-core"),
|
||||||
|
PlatformIdentifier("start/portmaster-start"),
|
||||||
|
PlatformIdentifier("notifier/portmaster-notifier"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Components, Assets and Data
|
||||||
|
identifiers = append(
|
||||||
|
identifiers,
|
||||||
|
|
||||||
|
// User interface components
|
||||||
|
PlatformIdentifier("app/portmaster-app.zip"),
|
||||||
|
"all/ui/modules/portmaster.zip",
|
||||||
|
"all/ui/modules/assets.zip",
|
||||||
|
|
||||||
|
// Filter lists data
|
||||||
|
"all/intel/lists/base.dsdl",
|
||||||
|
"all/intel/lists/intermediate.dsdl",
|
||||||
|
"all/intel/lists/urgent.dsdl",
|
||||||
|
|
||||||
|
// Geo IP data
|
||||||
|
"all/intel/geoip/geoipv4.mmdb.gz",
|
||||||
|
"all/intel/geoip/geoipv6.mmdb.gz",
|
||||||
|
)
|
||||||
|
|
||||||
|
return identifiers
|
||||||
|
}
|
||||||
|
|
||||||
|
// AutoUnpackUpdates returns assets that need unpacking.
|
||||||
|
func AutoUnpackUpdates() []string {
|
||||||
|
return []string{
|
||||||
|
PlatformIdentifier("app/portmaster-app.zip"),
|
||||||
|
}
|
||||||
|
}
|
||||||
105
updates/main.go
105
updates/main.go
@@ -4,11 +4,11 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"runtime"
|
"runtime"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/safing/portmaster/updates/helper"
|
||||||
|
|
||||||
"github.com/safing/portbase/dataroot"
|
"github.com/safing/portbase/dataroot"
|
||||||
"github.com/safing/portbase/log"
|
"github.com/safing/portbase/log"
|
||||||
"github.com/safing/portbase/modules"
|
"github.com/safing/portbase/modules"
|
||||||
@@ -19,10 +19,6 @@ import (
|
|||||||
const (
|
const (
|
||||||
onWindows = runtime.GOOS == "windows"
|
onWindows = runtime.GOOS == "windows"
|
||||||
|
|
||||||
releaseChannelKey = "core/releaseChannel"
|
|
||||||
releaseChannelStable = "stable"
|
|
||||||
releaseChannelBeta = "beta"
|
|
||||||
|
|
||||||
enableUpdatesKey = "core/automaticUpdates"
|
enableUpdatesKey = "core/automaticUpdates"
|
||||||
|
|
||||||
// ModuleName is the name of the update module
|
// ModuleName is the name of the update module
|
||||||
@@ -48,16 +44,11 @@ var (
|
|||||||
module *modules.Module
|
module *modules.Module
|
||||||
registry *updater.ResourceRegistry
|
registry *updater.ResourceRegistry
|
||||||
userAgentFromFlag string
|
userAgentFromFlag string
|
||||||
staging bool
|
|
||||||
|
|
||||||
updateTask *modules.Task
|
updateTask *modules.Task
|
||||||
updateASAP bool
|
updateASAP bool
|
||||||
disableTaskSchedule bool
|
disableTaskSchedule bool
|
||||||
|
|
||||||
// MandatoryUpdates is a list of full identifiers that
|
|
||||||
// should always be kept up to date.
|
|
||||||
MandatoryUpdates []string
|
|
||||||
|
|
||||||
// UserAgent is an HTTP User-Agent that is used to add
|
// UserAgent is an HTTP User-Agent that is used to add
|
||||||
// more context to requests made by the registry when
|
// more context to requests made by the registry when
|
||||||
// fetching resources from the update server.
|
// fetching resources from the update server.
|
||||||
@@ -65,9 +56,8 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
updateInProgress = "updates:in-progress"
|
updateFailed = "updates:failed"
|
||||||
updateFailed = "updates:failed"
|
updateSuccess = "updates:success"
|
||||||
updateSuccess = "updates:success"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@@ -76,29 +66,9 @@ func init() {
|
|||||||
module.RegisterEvent(ResourceUpdateEvent, true)
|
module.RegisterEvent(ResourceUpdateEvent, true)
|
||||||
|
|
||||||
flag.StringVar(&userAgentFromFlag, "update-agent", "", "set the user agent for requests to the update server")
|
flag.StringVar(&userAgentFromFlag, "update-agent", "", "set the user agent for requests to the update server")
|
||||||
flag.BoolVar(&staging, "staging", false, "use staging update channel; for testing only")
|
|
||||||
|
|
||||||
// initialize mandatory updates
|
var dummy bool
|
||||||
if onWindows {
|
flag.BoolVar(&dummy, "staging", false, "deprecated, configure in settings instead")
|
||||||
MandatoryUpdates = []string{
|
|
||||||
platform("core/portmaster-core.exe"),
|
|
||||||
platform("start/portmaster-start.exe"),
|
|
||||||
platform("notifier/portmaster-notifier.exe"),
|
|
||||||
platform("notifier/portmaster-snoretoast.exe"),
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
MandatoryUpdates = []string{
|
|
||||||
platform("core/portmaster-core"),
|
|
||||||
platform("start/portmaster-start"),
|
|
||||||
platform("notifier/portmaster-notifier"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MandatoryUpdates = append(
|
|
||||||
MandatoryUpdates,
|
|
||||||
platform("app/portmaster-app.zip"),
|
|
||||||
"all/ui/modules/portmaster.zip",
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func prep() error {
|
func prep() error {
|
||||||
@@ -107,8 +77,6 @@ func prep() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return registerAPIEndpoints()
|
return registerAPIEndpoints()
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func start() error {
|
func start() error {
|
||||||
@@ -131,13 +99,11 @@ func start() error {
|
|||||||
"https://updates.safing.io",
|
"https://updates.safing.io",
|
||||||
},
|
},
|
||||||
UserAgent: UserAgent,
|
UserAgent: UserAgent,
|
||||||
MandatoryUpdates: MandatoryUpdates,
|
MandatoryUpdates: helper.MandatoryUpdates(),
|
||||||
AutoUnpack: []string{
|
AutoUnpack: helper.AutoUnpackUpdates(),
|
||||||
platform("app/portmaster-app.zip"),
|
UsePreReleases: initialReleaseChannel != helper.ReleaseChannelStable,
|
||||||
},
|
DevMode: devMode(),
|
||||||
Beta: releaseChannel() == releaseChannelBeta,
|
Online: true,
|
||||||
DevMode: devMode(),
|
|
||||||
Online: true,
|
|
||||||
}
|
}
|
||||||
if userAgentFromFlag != "" {
|
if userAgentFromFlag != "" {
|
||||||
// override with flag value
|
// override with flag value
|
||||||
@@ -149,38 +115,8 @@ func start() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
registry.AddIndex(updater.Index{
|
// Set indexes based on the release channel.
|
||||||
Path: "stable.json",
|
helper.SetIndexes(registry, initialReleaseChannel)
|
||||||
Stable: true,
|
|
||||||
Beta: false,
|
|
||||||
})
|
|
||||||
|
|
||||||
if registry.Beta {
|
|
||||||
registry.AddIndex(updater.Index{
|
|
||||||
Path: "beta.json",
|
|
||||||
Stable: false,
|
|
||||||
Beta: true,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
registry.AddIndex(updater.Index{
|
|
||||||
Path: "all/intel/intel.json",
|
|
||||||
Stable: true,
|
|
||||||
Beta: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
if stagingActive() {
|
|
||||||
// Set flag no matter how staging was activated.
|
|
||||||
staging = true
|
|
||||||
|
|
||||||
log.Warning("updates: staging environment is active")
|
|
||||||
|
|
||||||
registry.AddIndex(updater.Index{
|
|
||||||
Path: "staging.json",
|
|
||||||
Stable: true,
|
|
||||||
Beta: true,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
err = registry.LoadIndexes(module.Ctx)
|
err = registry.LoadIndexes(module.Ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -342,21 +278,6 @@ func stop() error {
|
|||||||
return stopVersionExport()
|
return stopVersionExport()
|
||||||
}
|
}
|
||||||
|
|
||||||
func platform(identifier string) string {
|
|
||||||
return fmt.Sprintf("%s_%s/%s", runtime.GOOS, runtime.GOARCH, identifier)
|
|
||||||
}
|
|
||||||
|
|
||||||
func stagingActive() bool {
|
|
||||||
// Check flag and env variable.
|
|
||||||
if staging || os.Getenv("PORTMASTER_STAGING") == "enabled" {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if staging index is present and acessible.
|
|
||||||
_, err := os.Stat(filepath.Join(registry.StorageDir().Path, "staging.json"))
|
|
||||||
return err == nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// RootPath returns the root path used for storing updates.
|
// RootPath returns the root path used for storing updates.
|
||||||
func RootPath() string {
|
func RootPath() string {
|
||||||
if !module.Online() {
|
if !module.Online() {
|
||||||
|
|||||||
@@ -351,10 +351,9 @@ func upgradeFile(fileToUpgrade string, file *updater.File) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CopyFile atomically copies a file using the update registry's tmp dir.
|
// CopyFile atomically copies a file using the update registry's tmp dir.
|
||||||
func CopyFile(srcPath, dstPath string) (err error) {
|
func CopyFile(srcPath, dstPath string) error {
|
||||||
|
|
||||||
// check tmp dir
|
// check tmp dir
|
||||||
err = registry.TmpDir().Ensure()
|
err := registry.TmpDir().Ensure()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("could not prepare tmp directory for copying file: %w", err)
|
return fmt.Errorf("could not prepare tmp directory for copying file: %w", err)
|
||||||
}
|
}
|
||||||
@@ -369,14 +368,14 @@ func CopyFile(srcPath, dstPath string) (err error) {
|
|||||||
// open source
|
// open source
|
||||||
srcFile, err := os.Open(srcPath)
|
srcFile, err := os.Open(srcPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return err
|
||||||
}
|
}
|
||||||
defer srcFile.Close()
|
defer srcFile.Close()
|
||||||
|
|
||||||
// copy data
|
// copy data
|
||||||
_, err = io.Copy(atomicDstFile, srcFile)
|
_, err = io.Copy(atomicDstFile, srcFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// finalize file
|
// finalize file
|
||||||
|
|||||||
Reference in New Issue
Block a user