Implement loading and storing config to / from file

This commit is contained in:
Joshua Ramon Enslin 2025-02-25 04:00:30 +01:00
parent d4c83e27d2
commit 3cb49d005e
Signed by: jrenslin
GPG Key ID: 46016F84501B70AE
4 changed files with 386 additions and 13 deletions

View File

@ -51,5 +51,19 @@ func TestValidateInstanceWorks(t *testing.T) {
t.Fatalf("Output of ValidateInstanceLink() is not https://hessen.museum-digital.de where it should be")
}
}
// Test that ValidateInstanceLink() works with a valid instance of md and cleans paths.
func TestValidateInstanceDoesCleanPathFromUrl(t *testing.T) {
result, err := ValidateInstanceLink("https://hessen.museum-digital.de/home")
if err != nil {
t.Fatalf("ValidateInstanceLink() returns an error where it should work")
}
if result != "https://hessen.museum-digital.de" {
t.Fatalf("Output of ValidateInstanceLink() is not https://hessen.museum-digital.de where it should be")
}
}

View File

@ -25,8 +25,6 @@ func ValidateInstitutionId(institutionId int, instanceUrl string) (int, error) {
return 0, errors.New("The institution page does not respond with HTTP 200, the institution does not seem to exist")
}
return institutionId, nil
}

View File

@ -1,25 +1,212 @@
package configloader
import (
"os"
"encoding/json"
"path/filepath"
"io/ioutil"
)
type MDWebDavUploaderConfig struct {
InstanceLink string `json:"instance"`
Mail string `json:"mail"`
WebDavAuthToken string `json:"token"`
InstitutionId int `json:"institution_id"`
Parser string `json:"parser"`
MetadataFolder string `json:"metadata_folder"`
MediaFolder string `json:"media_folder"`
PublishOnImport bool `json:"visible"`
InstanceLink string `json:"instance"`
Mail string `json:"mail"`
WebDavAuthToken string `json:"token"`
InstitutionId int `json:"institution_id"`
Parser string `json:"parser"`
MetadataFolder string `json:"metadata_folder"`
MediaFolder string `json:"media_folder"`
PublishOnImport bool `json:"visible"`
Settings map[string]string `json:"settings"`
}
// Returns a uniform filepath for the configuration of this tool.
// Returns the file path of the configuration file within the supplied
// directory. Moved to a dedicated function to provide a consistent
// filename.
func getConfigFileNameByDir(folder string) string {
return filepath.Join(folder, "config.json")
}
// Returns a uniform directory for the configuration of this tool.
// To be compatible across operating systems, this will be a JSON
// file in the same directory as the current programm.
func getConfigFilepath() string {
func getConfigFilepath() (string, error) {
// Get the OS-dependent configuration directory.
generalConfigDir, dirErr := os.UserConfigDir()
if dirErr != nil {
return "", dirErr
}
// Select a subdirectory of that directory to store application-specific
// settings in. Attempt to create it and return the config filepath as
// a file in that directory.
configDir := filepath.Join(generalConfigDir, "museum-digital-uploader")
generateConfigDirErr := os.Mkdir(configDir, 0700)
// The config directory could be created, return path of the configuration
// file in it.
if generateConfigDirErr == nil {
return getConfigFileNameByDir(configDir), nil // Use this to create files
}
// There has been an error creating the config directory.
// This may be either that the directory already exists, which is alright
// (and even expected on most runs). Or it might be, that the path is
// already occupied with a file. Or something else. In those cases,
// the error should be returned.
// If the path is already occupied, inspect it more closely.
if os.IsExist(generateConfigDirErr) {
info, err := os.Stat(configDir)
if err != nil {
return "", err
}
// The directory already exists and is a directory. This is fine,
// so we return the config filename in the existing config directory.
if info.IsDir() {
return getConfigFileNameByDir(configDir), nil // Use this to create files
}
}
return "", generateConfigDirErr
}
// Wrapper around getConfigFilepath() allowing for an override for testing.
func getConfigFilepathOrOverride(overridePath string) (string, error) {
// Override path is not validated, as it should only be used
// in test settings
if overridePath != "" {
return overridePath, nil
}
configFilePath, err := getConfigFilepath()
if err != nil {
return "", err
}
return configFilePath, nil
}
// Validates each of the values of MDWebDavUploaderConfig.
func ValidateConfig(conf MDWebDavUploaderConfig) (MDWebDavUploaderConfig, error) {
// Validate and clean instance link
instanceLink, instanceErr := ValidateInstanceLink(conf.InstanceLink)
if instanceErr != nil {
return conf, instanceErr
}
conf.InstanceLink = instanceLink
// Validate and clean mail
mailLink, mailErr := ValidateMail(conf.Mail)
if mailErr != nil {
return conf, mailErr
}
conf.Mail = mailLink
// Validate and clean institution ID
institutionIdLink, institutionIdErr := ValidateInstitutionId(conf.InstitutionId, conf.InstanceLink)
if institutionIdErr != nil {
return conf, institutionIdErr
}
conf.InstitutionId = institutionIdLink
// Validate and clean parser
parserLink, parserErr := ValidateParser(conf.Parser)
if parserErr != nil {
return conf, parserErr
}
conf.Parser = parserLink
// Validate and clean metadata folder
metadataFolder, mFolderErr := ValidateUploadDir(conf.MetadataFolder)
if mFolderErr != nil {
return conf, mFolderErr
}
conf.MetadataFolder = metadataFolder
// Validate and clean media folder
mediaFolder, mediaFolderErr := ValidateUploadDir(conf.MediaFolder)
if mediaFolderErr != nil {
return conf, mediaFolderErr
}
conf.MediaFolder = mediaFolder
return conf, nil
}
// Loads configuration from the configuration file (located using
// getConfigFilepath()).
func LoadFromFile() MDWebDavUploaderConfig {
// The function parameter overridePath is only useful for test settings.
// Outside those, set an empty string to use the default config path.
// Output parameters:
// 1: Config
// 2: Requirement for a re-run of the setup
// 3: Error
func LoadFromFile(overridePath string) (MDWebDavUploaderConfig, bool, error) {
// Get config file path
configFile, configFileErr := getConfigFilepathOrOverride(overridePath)
if configFileErr != nil {
return MDWebDavUploaderConfig{}, false, configFileErr
}
// Read the file
configFileBytes, ioErr := ioutil.ReadFile(configFile)
if ioErr != nil {
return MDWebDavUploaderConfig{}, true, ioErr
}
// Parse the file into a configuration struct
var data MDWebDavUploaderConfig
unmarshalErr := json.Unmarshal(configFileBytes, &data)
if unmarshalErr != nil {
return MDWebDavUploaderConfig{}, true, ioErr
}
// Validate data - the configuration file may have been altered externally
config, validationErr := ValidateConfig(data)
if validationErr != nil {
return MDWebDavUploaderConfig{}, true, validationErr
}
// Config could be parsed
return config, false, nil
}
// Stores a config from MDWebDavUploaderConfig to the config file.
// As with LoadFromFile(), it is possible to provide an overridePath for testing.
func StoreConfigToFile(conf MDWebDavUploaderConfig, overridePath string) error {
config, validationErr := ValidateConfig(conf)
if validationErr != nil {
return validationErr
}
configFilePath, configFileErr := getConfigFilepathOrOverride(overridePath)
if configFileErr != nil {
return configFileErr
}
configJson, encodeErr := json.Marshal(config)
if encodeErr != nil {
return encodeErr
}
writeErr := ioutil.WriteFile(configFilePath, configJson, 0644)
if writeErr != nil {
return writeErr
}
return nil
}

View File

@ -0,0 +1,174 @@
package configloader
import (
"testing"
"os"
"path/filepath"
)
// Returns a generally valid config. Single values can then be
// replaced with the actually tested contents.
func getTestConfig() MDWebDavUploaderConfig {
input := MDWebDavUploaderConfig{}
input.InstanceLink = "https://hessen.museum-digital.de/home"
input.Mail = "test@example.com"
input.InstitutionId = 1;
input.Parser = "Lido"
tmpDir := os.TempDir()
testDir := filepath.Join(tmpDir, "/existing-dir-for-import")
mkdirErr := os.MkdirAll(testDir, os.ModePerm)
if mkdirErr != nil {
panic("Test failure: Failed to create test dir")
}
input.MetadataFolder = testDir
input.MediaFolder = testDir
return input
}
// Test that ValidateConfig() fails on non-URLs.
func TestValidateUploaderConfigFailsOnInvalidInstanceUrl(t *testing.T) {
input := getTestConfig()
input.InstanceLink = "abcmuseum-digital.org"
_, err := ValidateConfig(input)
if err == nil {
t.Fatalf("ValidateConfig() does not return an error on a non-URL (via ValidateInstanceLink)")
}
}
// Test that ValidateConfig cleans an instance URL as per ValidateInstanceLink().
func TestValidateUploaderConfigCleansInstanceViaValidateInstanceLink(t *testing.T) {
input := getTestConfig()
input.InstanceLink = "https://hessen.museum-digital.de/home"
returnVal, _ := ValidateConfig(input)
if returnVal.InstanceLink != "https://hessen.museum-digital.de" {
t.Fatalf("Failed to clean up input URL")
}
}
// Test that ValidateConfig() fails on invalid mail.
func TestValidateUploaderConfigFailsOnInvalidMail(t *testing.T) {
input := getTestConfig()
input.Mail = "test"
_, err := ValidateConfig(input)
if err == nil {
t.Fatalf("ValidateConfig() does not return an error on a mail address")
}
}
// Test that ValidateConfig() accepts a valid mail address.
func TestValidateUploaderConfigAcceptsValidMail(t *testing.T) {
input := getTestConfig()
input.Mail = "test@example.com"
returnVal, _ := ValidateConfig(input)
if returnVal.Mail != "test@example.com" {
t.Fatalf("Failed to accept valid mail")
}
}
// Test that ValidateConfig() fails on negative / invalid IDs.
func TestValidateUploaderConfigFailsOnInvalidInstitutionId(t *testing.T) {
input := getTestConfig()
input.InstitutionId = -1
_, err := ValidateConfig(input)
if err == nil {
t.Fatalf("ValidateConfig() does not return an error on an invalid institution ID")
}
}
// Test that ValidateConfig() accepts valid institution IDs.
func TestValidateUploaderConfigAcceptsValidInstitutionId(t *testing.T) {
input := getTestConfig()
input.InstitutionId = 1
returnVal, _ := ValidateConfig(input)
if returnVal.InstitutionId != 1 {
t.Fatalf("Failed to accept valid institution ID")
}
}
// Test that ValidateConfig() fails on negative / invalid IDs.
func TestValidateUploaderConfigFailsOnInvalidParser(t *testing.T) {
input := getTestConfig()
input.Parser = "nonexistentparser"
_, err := ValidateConfig(input)
if err == nil {
t.Fatalf("ValidateConfig() does not return an error on an invalid parser")
}
}
// Test that ValidateConfig() accepts valid ParserDs.
func TestValidateUploaderConfigAcceptsValidParser(t *testing.T) {
input := getTestConfig()
input.Parser = "ParserLido"
returnVal, _ := ValidateConfig(input)
if returnVal.Parser != "Lido" {
t.Fatalf("Failed to accept and clean valid parser")
}
}
// Test that ValidateConfig() fails on non-existent folder.
func TestValidateUploaderConfigFailsOnInvalidMetadataFolder(t *testing.T) {
input := getTestConfig()
input.MetadataFolder = "nonexistentfolder"
_, err := ValidateConfig(input)
if err == nil {
t.Fatalf("ValidateConfig() does not return an error on an invalid metadata folder")
}
}
// Test that saving and reading config works.
func TestWritingAndReadingConfigWorks(t *testing.T) {
input := getTestConfig()
input, _ = ValidateConfig(input)
writeErr := StoreConfigToFile(input, input.MetadataFolder + "/config.json")
if writeErr != nil {
t.Log("Error:")
t.Log(writeErr)
t.Fatalf("Failed to write config to override path")
}
loadedFromFile, setupRequired, err := LoadFromFile(input.MetadataFolder + "/config.json")
if setupRequired != false {
t.Fatalf("Expected no setup to be required, but return value indicated thus")
}
if err != nil {
t.Fatalf("Returned an error on trying to load config file")
}
// Golang can't compare structs with slices or maps
if input.InstanceLink != loadedFromFile.InstanceLink || input.Mail != loadedFromFile.Mail || input.WebDavAuthToken != loadedFromFile.WebDavAuthToken || input.InstitutionId != loadedFromFile.InstitutionId || input.Parser != loadedFromFile.Parser || input.MetadataFolder != loadedFromFile.MetadataFolder || input.MediaFolder != loadedFromFile.MediaFolder || input.PublishOnImport != loadedFromFile.PublishOnImport {
t.Log("Input")
t.Log(input)
t.Log("Loaded output")
t.Log(loadedFromFile)
t.Fatalf("Failed to write and then load the same config")
}
}