diff --git a/cmd/sf/conf.go b/cmd/sf/conf.go index d6218b2fd..74c348d9e 100644 --- a/cmd/sf/conf.go +++ b/cmd/sf/conf.go @@ -17,8 +17,10 @@ package main import ( "bufio" "bytes" + "errors" "flag" "fmt" + "io/fs" "io/ioutil" "os" "strings" @@ -59,7 +61,7 @@ func setconf() (string, error) { } // no flags - so we delete the conf file if it exists if _, err := os.Stat(config.Conf()); err != nil { - if os.IsNotExist(err) { + if errors.Is(err, fs.ErrNotExist) { return "", nil } return "", err @@ -70,7 +72,7 @@ func setconf() (string, error) { // if it exists, read defaults from the conf file. func getconf() (map[string]string, error) { if _, err := os.Stat(config.Conf()); err != nil { - if os.IsNotExist(err) { + if errors.Is(err, fs.ErrNotExist) { return nil, nil } return nil, err diff --git a/cmd/sf/update.go b/cmd/sf/update.go index cbf320bc8..17b693b9b 100644 --- a/cmd/sf/update.go +++ b/cmd/sf/update.go @@ -20,7 +20,9 @@ import ( "crypto/sha256" "encoding/hex" "encoding/json" + "errors" "fmt" + "io/fs" "io/ioutil" "net/http" "os" @@ -118,7 +120,7 @@ func updateSigs(sig string, args []string) (bool, string, error) { } // this hairy bit of golang exception handling is thanks to Ross! :) if _, err = os.Stat(config.Home()); err != nil { - if os.IsNotExist(err) { + if errors.Is(err, fs.ErrNotExist) { err = os.MkdirAll(config.Home(), os.ModePerm) if err != nil { return false, "", fmt.Errorf("Siegfried: cannot create home directory %s, %v", config.Home(), err) diff --git a/pkg/config/default.go b/pkg/config/default.go index c899df566..cd00aec54 100644 --- a/pkg/config/default.go +++ b/pkg/config/default.go @@ -1,3 +1,4 @@ +//go:build !brew && !archivematica && !js // +build !brew,!archivematica,!js // Copyright 2014 Richard Lehane. All rights reserved. @@ -17,16 +18,53 @@ package config import ( + "errors" + "io/fs" "log" - "os/user" + "os" "path/filepath" + "strings" ) -// the default Home location is a "siegfried" folder in the user's $HOME +// the default Home location is a "siegfried" folder in the user's application data folder, which can be overridden by setting the SIEGFRIED_HOME environment variable func init() { - current, err := user.Current() - if err != nil { - log.Fatal(err) + if home, ok := os.LookupEnv("SIEGFRIED_HOME"); ok { + siegfried.home = home + } else { + home, err := os.UserHomeDir() + if err != nil { + log.Fatal(err) + } + + // if a home directory already exists in the legacy location continue using it, otherwise default to a XDG-aware OS-specific application data directory + siegfried.home = filepath.Join(home, "siegfried") + if _, err := os.Stat(siegfried.home); err != nil { + if errors.Is(err, fs.ErrNotExist) { + siegfried.home = filepath.Join(userDataDir(home), "siegfried") + } else { + log.Fatal(err) + } + } + } +} + +func xdgPath(home string, defaultPath string) string { + dataHome, found := os.LookupEnv("XDG_DATA_HOME") + if found && dataHome != "" { + if dataDir, found := strings.CutPrefix(dataHome, "~"); found { + dataHome = filepath.Join(home, dataDir) + } + + // environment variable might contain variables like $HOME itself, let's expand + dataHome = os.ExpandEnv(dataHome) + } + + // XDG Base Directory Specification demands relative paths to be ignored, fall back to default in that case + if filepath.IsAbs(dataHome) { + return dataHome + } else if filepath.IsAbs(defaultPath) { + return defaultPath + } else { + return filepath.Join(home, defaultPath) } - siegfried.home = filepath.Join(current.HomeDir, "siegfried") } diff --git a/pkg/config/default_darwin.go b/pkg/config/default_darwin.go new file mode 100644 index 000000000..2556ca772 --- /dev/null +++ b/pkg/config/default_darwin.go @@ -0,0 +1,27 @@ +//go:build darwin && !brew && !archivematica && !js +// +build darwin,!brew,!archivematica,!js + +// Copyright 2014 Richard Lehane. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package config + +import ( + "path/filepath" +) + + +func userDataDir(home string) string { + return xdgPath(home, filepath.Join("Library", "Application Support")) +} diff --git a/pkg/config/default_plan9.go b/pkg/config/default_plan9.go new file mode 100644 index 000000000..78291fbfd --- /dev/null +++ b/pkg/config/default_plan9.go @@ -0,0 +1,23 @@ +//go:build plan9 && !brew && !archivematica && !js +// +build plan9,!brew,!archivematica,!js + +// Copyright 2014 Richard Lehane. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package config + + +func userDataDir(home string) string { + return xdgPath(home, "lib") +} diff --git a/pkg/config/default_unix.go b/pkg/config/default_unix.go new file mode 100644 index 000000000..255c00896 --- /dev/null +++ b/pkg/config/default_unix.go @@ -0,0 +1,26 @@ +//go:build (aix || darwin || dragonfly || freebsd || nacl || linux || netbsd || openbsd || solaris) && !brew && !archivematica && !js +// +build aix darwin dragonfly freebsd nacl linux netbsd openbsd solaris +// +build !brew +// +build !archivematica +// +build !js + +// Copyright 2014 Richard Lehane. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package config + + +func userDataDir(home string) string { + return xdgPath(home, ".local/share") +} diff --git a/pkg/config/default_windows.go b/pkg/config/default_windows.go new file mode 100644 index 000000000..8799e7f22 --- /dev/null +++ b/pkg/config/default_windows.go @@ -0,0 +1,44 @@ +//go:build windows && !brew && !archivematica && !js +// +build windows,!brew,!archivematica,!js + +// Copyright 2014 Richard Lehane. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package config + +import ( + "os" + "path/filepath" + + "golang.org/x/sys/windows" +) + + +func userDataDir(home string) string { + path, _ := windows.KnownFolderPath(*windows.FOLDERID_LocalAppData, windows.KF_FLAG_DEFAULT|KF_FLAG_DONT_VERIFY) + if path == "" { + path, _ = windows.KnownFolderPath(*windows.FOLDERID_LocalAppData, windows.KF_FLAG_DEFAULT_PATH|KF_FLAG_DONT_VERIFY) + } + + if path == "" { + dataDir, found := os.LookupEnv("LOCALAPPDATA") + if found && dataDir != "" { + path = dataDir + } else { + path = filepath.Join("AppData", "Local") + } + } + + return xdgPath(home, path) +} diff --git a/pkg/config/mimeinfo.go b/pkg/config/mimeinfo.go index 4ccac1947..b2c771be9 100644 --- a/pkg/config/mimeinfo.go +++ b/pkg/config/mimeinfo.go @@ -16,7 +16,7 @@ package config import ( "encoding/json" - "io/ioutil" + "os" "path/filepath" ) @@ -49,7 +49,7 @@ func MIMEInfo() string { } func MIMEVersion() []string { - byt, err := ioutil.ReadFile(filepath.Join(siegfried.home, mimeinfo.versions)) + byt, err := os.ReadFile(filepath.Join(siegfried.home, mimeinfo.versions)) m := make(map[string][]string) if err == nil { err = json.Unmarshal(byt, &m) diff --git a/pkg/config/pronom.go b/pkg/config/pronom.go index 06a4fd72e..0368fbc4e 100644 --- a/pkg/config/pronom.go +++ b/pkg/config/pronom.go @@ -16,8 +16,8 @@ package config import ( "fmt" - "io/ioutil" "net/http" + "os" "path/filepath" "strconv" "strings" @@ -125,7 +125,7 @@ func ContainerBase() string { func latest(prefix, suffix string) (string, error) { var hits []string var ids []int - files, err := ioutil.ReadDir(siegfried.home) + files, err := os.ReadDir(siegfried.home) if err != nil { return "", err } @@ -141,7 +141,7 @@ func latest(prefix, suffix string) (string, error) { } } if len(hits) == 0 { - return "", fmt.Errorf("Config: no file in %s with prefix %s", siegfried.home, prefix) + return "", fmt.Errorf("config: no file in %s with prefix %s", siegfried.home, prefix) } if len(hits) == 1 { return hits[0], nil diff --git a/pkg/config/wikidata.go b/pkg/config/wikidata.go index 70c7ab7ac..79ff44d0a 100644 --- a/pkg/config/wikidata.go +++ b/pkg/config/wikidata.go @@ -18,7 +18,9 @@ package config import ( + "errors" "fmt" + "io/fs" "net/url" "os" "path/filepath" @@ -324,7 +326,7 @@ func SetWikibaseSparql(query string) func() private { func checkWikibaseURL(customEndpointURL string, customWikibaseURL string) error { if customWikibaseURL == WikidataWikibaseURL() { return fmt.Errorf( - "Wikibase server URL for '%s' needs to be configured, can't be: '%s'", + "wikibase server URL for '%s' needs to be configured, can't be: '%s'", customEndpointURL, customWikibaseURL, ) @@ -355,17 +357,18 @@ func SetCustomWikibaseEndpoint(customEndpointURL string, customWikibaseURL strin func SetCustomWikibaseQuery() error { wikibaseSparqlPath := WikibaseSparqlFile() sparqlFile, err := os.ReadFile(wikibaseSparqlPath) - if os.IsNotExist(err) { - return fmt.Errorf( - "Setting custom Wikibase SPARQL: cannot find file '%s' in '%s': %w", - wikibaseSparqlPath, - WikidataHome(), - err, - ) - } if err != nil { + if errors.Is(err, fs.ErrNotExist) { + return fmt.Errorf( + "setting custom Wikibase SPARQL: cannot find file '%s' in '%s': %w", + wikibaseSparqlPath, + WikidataHome(), + err, + ) + } + return fmt.Errorf( - "Setting custom Wikibase SPARQL: unexpected error opening '%s' has occurred: %w", + "setting custom Wikibase SPARQL: unexpected error opening '%s' has occurred: %w", wikibaseSparqlPath, err, ) diff --git a/pkg/config/wikidata_test.go b/pkg/config/wikidata_test.go index 52474f283..bb22b7353 100644 --- a/pkg/config/wikidata_test.go +++ b/pkg/config/wikidata_test.go @@ -3,7 +3,6 @@ package config import ( "errors" "io/fs" - "io/ioutil" "os" "path/filepath" "testing" @@ -42,15 +41,17 @@ func TestProps(t *testing.T) { // handling required for updating our SPARQL query for a custom Wikibase. func TestSetCustomWikibaseQuery(t *testing.T) { var testSPARQL = "select ?s ?p ?o where { ?s ?p ?o. }" - tempDir, _ := ioutil.TempDir("", "wikidata-test-dir-*") - defer os.RemoveAll(tempDir) - err := os.Mkdir(filepath.Join(tempDir, "wikidata"), 0755) + tempDir, err := os.MkdirTemp("", "wikidata-test-dir-*") + if err != nil { + t.Fatal(err) + } + err = os.Mkdir(filepath.Join(tempDir, "wikidata"), 0755) if err != nil { t.Fatal(err) } SetHome(tempDir) customSPARQLFile := filepath.Join(tempDir, "wikidata", "wikibase.sparql") - err = ioutil.WriteFile(customSPARQLFile, []byte(testSPARQL), 0755) + err = os.WriteFile(customSPARQLFile, []byte(testSPARQL), 0755) if err != nil { t.Fatal(err) } @@ -67,7 +68,7 @@ func TestSetCustomWikibaseQuery(t *testing.T) { WikidataSPARQL(), ) } - err = os.Remove(customSPARQLFile) + _ = os.Remove(customSPARQLFile) err = SetCustomWikibaseQuery() if !errors.Is(err, fs.ErrNotExist) { t.Errorf( diff --git a/pkg/wikidata/load_wikidata.go b/pkg/wikidata/load_wikidata.go index 66e501c39..07f53d5a0 100644 --- a/pkg/wikidata/load_wikidata.go +++ b/pkg/wikidata/load_wikidata.go @@ -22,6 +22,7 @@ import ( "encoding/json" "errors" "fmt" + "io/fs" "io/ioutil" "log" "os" @@ -269,15 +270,16 @@ func setCustomWikibaseProperties() error { logln("Roy (Wikidata): Looking for existence of wikibase.json in Siegfried home") wikibasePropsPath := config.WikibasePropsPath() propsFile, err := os.ReadFile(wikibasePropsPath) - if os.IsNotExist(err) { - return fmt.Errorf( - "cannot find file '%s' in '%s': %w", - wikibasePropsPath, - config.WikidataHome(), - err, - ) - } if err != nil { + if errors.Is(err, fs.ErrNotExist) { + return fmt.Errorf( + "cannot find file '%s' in '%s': %w", + wikibasePropsPath, + config.WikidataHome(), + err, + ) + } + return fmt.Errorf( "a different error handling '%s' has occurred: %w", wikibasePropsPath,