diff --git a/config/config.go b/config/config.go index 90236af..a325a59 100644 --- a/config/config.go +++ b/config/config.go @@ -12,9 +12,12 @@ package config import ( _ "embed" + "errors" "fmt" "maps" "os" + "regexp" + "strconv" argparse "github.com/alexflint/go-arg" "gopkg.in/yaml.v3" @@ -95,6 +98,11 @@ type AmConfig struct { } `yaml:"posting"` } +type AmConfigComputed struct { + UploadMaxSize int32 + UploadNoCompress map[string]bool +} + //go:embed default.yaml var defaultConfigData []byte @@ -104,6 +112,9 @@ var defaultConfig AmConfig // GlobalConfig holds the global configuration. var GlobalConfig AmConfig +// GlobalComputedConfig holds the computed values based on GlobalConfig. +var GlobalComputedConfig AmConfigComputed + // init prepares the default configuration for the application. func init() { if err := yaml.Unmarshal(defaultConfigData, &defaultConfig); err != nil { @@ -125,6 +136,14 @@ func overlayString(loaded string, defaulted string) string { return loaded } +/* overlayString is a helper that takes a loaded or defaulted string array and returns it. (It merges the two + * if two different arrays are specified.) + * Parameters: + * loaded - The array loaded from a configuration file. + * defaulted - The default value of this array. + * Returns: + * Merged version of the two arrays. + */ func overlayStringArray(loaded, defaulted []string) []string { m := make(map[string]bool) for _, s := range defaulted { @@ -191,6 +210,31 @@ func overlayConfig(dest *AmConfig, loaded *AmConfig, defaults *AmConfig) { dest.Posting.Uploads.NoCompressTypes = overlayStringArray(loaded.Posting.Uploads.NoCompressTypes, defaults.Posting.Uploads.NoCompressTypes) } +// parseDataSize converts the data size in bytes, kilobytes, megabytes, or gigabytes to a number value. +func parseDataSize(s string) (int32, error) { + re, err := regexp.Compile(`^\s*(\d+)\s*([KkMmGg]?)[Bb]?`) + if err != nil { + return -1, err + } + m := re.FindStringSubmatch(s) + if m == nil { + return -1, errors.New("invalid value spacified") + } + rc, err := strconv.Atoi(m[1]) + if err != nil { + return -1, err + } + switch m[2] { + case "k", "K": + rc *= 1024 + case "m", "M": + rc *= (1024 * 1024) + case "g", "G": + rc *= (1024 * 1024 * 1024) + } + return int32(rc), nil +} + // SetupConfig loads the command line arguments, loads the config file, and prepares GlobalConfig. func SetupConfig() { argparse.MustParse(&CommandLine) @@ -209,4 +253,15 @@ func SetupConfig() { } else { GlobalConfig = defaultConfig // just copy over the defaults } + + // Compute additional values. + tmp, err := parseDataSize(GlobalConfig.Posting.Uploads.MaxSize) + if err != nil { + panic(err.Error()) + } + GlobalComputedConfig.UploadMaxSize = tmp + GlobalComputedConfig.UploadNoCompress = make(map[string]bool) + for _, s := range GlobalConfig.Posting.Uploads.NoCompressTypes { + GlobalComputedConfig.UploadNoCompress[s] = true + } } diff --git a/database/post.go b/database/post.go index c902737..c62d82d 100644 --- a/database/post.go +++ b/database/post.go @@ -10,10 +10,14 @@ package database import ( + "bytes" + "compress/gzip" "context" "errors" "fmt" "time" + + "git.erbosoft.com/amy/amsterdam/config" ) // PostHeader represents the "header" of a post, everything except for its text and attachment. @@ -115,9 +119,33 @@ func (p *PostHeader) SetAttachment(ctx context.Context, u *User, fileName string if ai != nil { return errors.New("attachment already present for this post") } - // TODO - _, err = amdb.ExecContext(ctx, "INSERT INTO postattach (postid, datalen, filename, mimetype, data) VALUES (?, ?, ?, ?, ?)", - p.PostId, length, fileName, mimeType, data) + if length > config.GlobalComputedConfig.UploadMaxSize { + return fmt.Errorf("file too large to be attached; maximum size is %s", config.GlobalConfig.Posting.Uploads.MaxSize) + } + + // Compress the data with GZIP if we need to. + var stgmethod int16 + var realData []byte + if _, ok := config.GlobalComputedConfig.UploadNoCompress[mimeType]; ok { + realData = data + stgmethod = stgMethodPlain + } else { + buf := new(bytes.Buffer) + w := gzip.NewWriter(buf) + _, err := w.Write(data) + if err == nil { + err = w.Close() + } + if err != nil { + return err + } + realData = buf.Bytes() + stgmethod = stgMethodGZIP + } + + // Write to the database. + _, err = amdb.ExecContext(ctx, "INSERT INTO postattach (postid, datalen, filename, mimetype, stgmethod, data) VALUES (?, ?, ?, ?, ?, ?)", + p.PostId, length, fileName, mimeType, stgmethod, realData) return err }