[feature] configurable maximum thumbnail dimensions (#4258)

- adds configuration for thumbnail maximum dimensions with warning on exceeding recommendations
- moves the media configuration vars into their own sub-struct
- replaces the configuration flag funcs with simple string consts

Reviewed-on: https://codeberg.org/superseriousbusiness/gotosocial/pulls/4258
Reviewed-by: tobi <kipvandenbos@noreply.codeberg.org>
Co-authored-by: kim <grufwub@gmail.com>
Co-committed-by: kim <grufwub@gmail.com>
This commit is contained in:
kim 2025-06-10 15:43:31 +02:00 committed by kim
commit d7f967cbb5
17 changed files with 902 additions and 1099 deletions

View file

@ -620,14 +620,14 @@ func parseClientRanges() (
allowIPs := config.GetHTTPClientAllowIPs()
allowRanges := make([]netip.Prefix, len(allowIPs))
allowFlag := config.HTTPClientAllowIPsFlag()
allowFlag := config.HTTPClientAllowIPsFlag
if err := parseF(allowIPs, allowRanges, allowFlag); err != nil {
return nil, err
}
blockIPs := config.GetHTTPClientBlockIPs()
blockRanges := make([]netip.Prefix, len(blockIPs))
blockFlag := config.HTTPClientBlockIPsFlag()
blockFlag := config.HTTPClientBlockIPsFlag
if err := parseF(blockIPs, blockRanges, blockFlag); err != nil {
return nil, err
}

View file

@ -629,6 +629,13 @@ media-video-size-hint: 40MiB
# Default: 40MiB (41943040 bytes)
media-remote-max-size: 40MiB
# Int. Max size in pixels of any one dimension of
# a thumbnail (as input media ratio is preserved).
#
# Examples: [256, 512, 1024]
# Default: 512
media-thumb-max-pixels: 512
# Int. Minimum amount of characters required as an image or video description.
# Examples: [500, 1000, 1500]
# Default: 0 (not required)

View file

@ -47,7 +47,7 @@ func NewCookiePolicy() CookiePolicy {
case "lax":
sameSite = http.SameSiteLaxMode
default:
log.Warnf(nil, "%s set to %s which is not recognized, defaulting to 'lax'", config.AdvancedCookiesSamesiteFlag(), s)
log.Warnf(nil, "%s set to %s which is not recognized, defaulting to 'lax'", config.AdvancedCookiesSamesiteFlag, s)
sameSite = http.SameSiteLaxMode
}
return CookiePolicy{

View file

@ -113,19 +113,6 @@ type Configuration struct {
AccountsCustomCSSLength int `name:"accounts-custom-css-length" usage:"Maximum permitted length (characters) of custom CSS for accounts."`
AccountsMaxProfileFields int `name:"accounts-max-profile-fields" usage:"Maximum number of profile fields allowed for each account."`
MediaDescriptionMinChars int `name:"media-description-min-chars" usage:"Min required chars for an image description"`
MediaDescriptionMaxChars int `name:"media-description-max-chars" usage:"Max permitted chars for an image description"`
MediaRemoteCacheDays int `name:"media-remote-cache-days" usage:"Number of days to locally cache media from remote instances. If set to 0, remote media will be kept indefinitely."`
MediaEmojiLocalMaxSize bytesize.Size `name:"media-emoji-local-max-size" usage:"Max size in bytes of emojis uploaded to this instance via the admin API."`
MediaEmojiRemoteMaxSize bytesize.Size `name:"media-emoji-remote-max-size" usage:"Max size in bytes of emojis to download from other instances."`
MediaImageSizeHint bytesize.Size `name:"media-image-size-hint" usage:"Size in bytes of max image size referred to on /api/v_/instance endpoints (else, local max size)"`
MediaVideoSizeHint bytesize.Size `name:"media-video-size-hint" usage:"Size in bytes of max video size referred to on /api/v_/instance endpoints (else, local max size)"`
MediaLocalMaxSize bytesize.Size `name:"media-local-max-size" usage:"Max size in bytes of media uploaded to this instance via API"`
MediaRemoteMaxSize bytesize.Size `name:"media-remote-max-size" usage:"Max size in bytes of media to download from other instances"`
MediaCleanupFrom string `name:"media-cleanup-from" usage:"Time of day from which to start running media cleanup/prune jobs. Should be in the format 'hh:mm:ss', eg., '15:04:05'."`
MediaCleanupEvery time.Duration `name:"media-cleanup-every" usage:"Period to elapse between cleanups, starting from media-cleanup-at."`
MediaFfmpegPoolSize int `name:"media-ffmpeg-pool-size" usage:"Number of instances of the embedded ffmpeg WASM binary to add to the media processing pool. 0 or less uses GOMAXPROCS."`
StorageBackend string `name:"storage-backend" usage:"Storage backend to use for media attachments"`
StorageLocalBasePath string `name:"storage-local-base-path" usage:"Full path to an already-created directory where gts should store/retrieve media files. Subfolders will be created within this dir."`
StorageS3Endpoint string `name:"storage-s3-endpoint" usage:"S3 Endpoint URL (e.g 'minio.example.org:9000')"`
@ -181,6 +168,9 @@ type Configuration struct {
// HTTPClient configuration vars.
HTTPClient HTTPClientConfiguration `name:"http-client"`
// Media configuration vars.
Media MediaConfiguration `name:"media"`
// Cache configuration vars.
Cache CacheConfiguration `name:"cache"`
@ -202,6 +192,22 @@ type HTTPClientConfiguration struct {
InsecureOutgoing bool `name:"insecure-outgoing"`
}
type MediaConfiguration struct {
DescriptionMinChars int `name:"description-min-chars" usage:"Min required chars for an image description"`
DescriptionMaxChars int `name:"description-max-chars" usage:"Max permitted chars for an image description"`
RemoteCacheDays int `name:"remote-cache-days" usage:"Number of days to locally cache media from remote instances. If set to 0, remote media will be kept indefinitely."`
EmojiLocalMaxSize bytesize.Size `name:"emoji-local-max-size" usage:"Max size in bytes of emojis uploaded to this instance via the admin API."`
EmojiRemoteMaxSize bytesize.Size `name:"emoji-remote-max-size" usage:"Max size in bytes of emojis to download from other instances."`
ImageSizeHint bytesize.Size `name:"image-size-hint" usage:"Size in bytes of max image size referred to on /api/v_/instance endpoints (else, local max size)"`
VideoSizeHint bytesize.Size `name:"video-size-hint" usage:"Size in bytes of max video size referred to on /api/v_/instance endpoints (else, local max size)"`
LocalMaxSize bytesize.Size `name:"local-max-size" usage:"Max size in bytes of media uploaded to this instance via API"`
RemoteMaxSize bytesize.Size `name:"remote-max-size" usage:"Max size in bytes of media to download from other instances"`
CleanupFrom string `name:"cleanup-from" usage:"Time of day from which to start running media cleanup/prune jobs. Should be in the format 'hh:mm:ss', eg., '15:04:05'."`
CleanupEvery time.Duration `name:"cleanup-every" usage:"Period to elapse between cleanups, starting from media-cleanup-at."`
FfmpegPoolSize int `name:"ffmpeg-pool-size" usage:"Number of instances of the embedded ffmpeg WASM binary to add to the media processing pool. 0 or less uses GOMAXPROCS."`
ThumbMaxPixels int `name:"thumb-max-pixels" usage:"Max size in pixels of any one dimension of a thumbnail (as input media ratio is preserved)."`
}
type CacheConfiguration struct {
MemoryTarget bytesize.Size `name:"memory-target"`
AccountMemRatio float64 `name:"account-mem-ratio"`

View file

@ -78,16 +78,19 @@ var Defaults = Configuration{
AccountsCustomCSSLength: 10000,
AccountsMaxProfileFields: 6,
MediaDescriptionMinChars: 0,
MediaDescriptionMaxChars: 1500,
MediaRemoteCacheDays: 7,
MediaLocalMaxSize: 40 * bytesize.MiB,
MediaRemoteMaxSize: 40 * bytesize.MiB,
MediaEmojiLocalMaxSize: 50 * bytesize.KiB,
MediaEmojiRemoteMaxSize: 100 * bytesize.KiB,
MediaCleanupFrom: "00:00", // Midnight.
MediaCleanupEvery: 24 * time.Hour, // 1/day.
MediaFfmpegPoolSize: 1,
Media: MediaConfiguration{
DescriptionMinChars: 0,
DescriptionMaxChars: 1500,
RemoteCacheDays: 7,
LocalMaxSize: 40 * bytesize.MiB,
RemoteMaxSize: 40 * bytesize.MiB,
EmojiLocalMaxSize: 50 * bytesize.KiB,
EmojiRemoteMaxSize: 100 * bytesize.KiB,
CleanupFrom: "00:00", // Midnight.
CleanupEvery: 24 * time.Hour, // 1/day.
FfmpegPoolSize: 1,
ThumbMaxPixels: 512,
},
StorageBackend: "local",
StorageLocalBasePath: "/gotosocial/storage",

View file

@ -25,7 +25,7 @@ import (
// AddAdminAccount attaches flags pertaining to admin account actions.
func AddAdminAccount(cmd *cobra.Command) {
name := AdminAccountUsernameFlag()
name := AdminAccountUsernameFlag
usage := fieldtag("AdminAccountUsername", "usage")
cmd.Flags().String(name, "", usage) // REQUIRED
if err := cmd.MarkFlagRequired(name); err != nil {
@ -35,7 +35,7 @@ func AddAdminAccount(cmd *cobra.Command) {
// AddAdminAccountPassword attaches flags pertaining to admin account password reset.
func AddAdminAccountPassword(cmd *cobra.Command) {
name := AdminAccountPasswordFlag()
name := AdminAccountPasswordFlag
usage := fieldtag("AdminAccountPassword", "usage")
cmd.Flags().String(name, "", usage) // REQUIRED
if err := cmd.MarkFlagRequired(name); err != nil {
@ -49,7 +49,7 @@ func AddAdminAccountCreate(cmd *cobra.Command) {
AddAdminAccount(cmd)
AddAdminAccountPassword(cmd)
name := AdminAccountEmailFlag()
name := AdminAccountEmailFlag
usage := fieldtag("AdminAccountEmail", "usage")
cmd.Flags().String(name, "", usage) // REQUIRED
if err := cmd.MarkFlagRequired(name); err != nil {
@ -59,7 +59,7 @@ func AddAdminAccountCreate(cmd *cobra.Command) {
// AddAdminTrans attaches flags pertaining to import/export commands.
func AddAdminTrans(cmd *cobra.Command) {
name := AdminTransPathFlag()
name := AdminTransPathFlag
usage := fieldtag("AdminTransPath", "usage")
cmd.Flags().String(name, "", usage) // REQUIRED
if err := cmd.MarkFlagRequired(name); err != nil {
@ -69,18 +69,18 @@ func AddAdminTrans(cmd *cobra.Command) {
// AddAdminMediaList attaches flags pertaining to media list commands.
func AddAdminMediaList(cmd *cobra.Command) {
localOnly := AdminMediaListLocalOnlyFlag()
localOnly := AdminMediaListLocalOnlyFlag
localOnlyUsage := fieldtag("AdminMediaListLocalOnly", "usage")
cmd.Flags().Bool(localOnly, false, localOnlyUsage)
remoteOnly := AdminMediaListRemoteOnlyFlag()
remoteOnly := AdminMediaListRemoteOnlyFlag
remoteOnlyUsage := fieldtag("AdminMediaListRemoteOnly", "usage")
cmd.Flags().Bool(remoteOnly, false, remoteOnlyUsage)
}
// AddAdminMediaPrune attaches flags pertaining to media storage prune commands.
func AddAdminMediaPrune(cmd *cobra.Command) {
name := AdminMediaPruneDryRunFlag()
name := AdminMediaPruneDryRunFlag
usage := fieldtag("AdminMediaPruneDryRun", "usage")
cmd.Flags().Bool(name, true, usage)
}

View file

@ -85,6 +85,7 @@ func main() {
fprintf(output, "\t\"github.com/spf13/cast\"\n")
fprintf(output, ")\n")
fprintf(output, "\n")
generateFlagConsts(output, fields)
generateFlagRegistering(output, fields)
generateMapMarshaler(output, fields)
generateMapUnmarshaler(output, fields)
@ -200,14 +201,14 @@ func loadConfigFields(pathPrefixes, flagPrefixes []string, t reflect.Type) []Con
return out
}
// func generateFlagConsts(out io.Writer, fields []ConfigField) {
// fprintf(out, "const (\n")
// for _, field := range fields {
// name := strings.ReplaceAll(field.Path, ".", "")
// fprintf(out, "\t%sFlag = \"%s\"\n", name, field.Flag())
// }
// fprintf(out, ")\n\n")
// }
func generateFlagConsts(out io.Writer, fields []ConfigField) {
fprintf(out, "const (\n")
for _, field := range fields {
name := strings.ReplaceAll(field.Path, ".", "")
fprintf(out, "\t%sFlag = \"%s\"\n", name, field.Flag())
}
fprintf(out, ")\n\n")
}
func generateFlagRegistering(out io.Writer, fields []ConfigField) {
fprintf(out, "func (cfg *Configuration) RegisterFlags(flags *pflag.FlagSet) {\n")
@ -461,9 +462,6 @@ func generateGetSetters(out io.Writer, fields []ConfigField) {
"config.", "",
)
fprintf(out, "// %sFlag returns the flag name for the '%s' field\n", name, field.Path)
fprintf(out, "func %sFlag() string { return \"%s\" }\n\n", name, field.Flag())
// ConfigState structure helper methods
fprintf(out, "// Get%s safely fetches the Configuration value for state's '%s' field\n", name, field.Path)
fprintf(out, "func (st *ConfigState) Get%s() (v %s) {\n", name, fieldType)

File diff suppressed because it is too large Load diff

View file

@ -42,7 +42,7 @@ func Validate() error {
// `host`
host := GetHost()
if host == "" {
errf("%s must be set", HostFlag())
errf("%s must be set", HostFlag)
}
// If `account-domain` and `host`
@ -55,10 +55,8 @@ func Validate() error {
// back by setting it to `host`.
SetAccountDomain(GetHost())
} else if !dns.IsSubDomain(ad, host) {
errf(
"%s %s is not a valid subdomain of %s %s",
AccountDomainFlag(), ad, HostFlag(), host,
)
errf("%s %s is not a valid subdomain of %s %s",
AccountDomainFlag, ad, HostFlag, host)
}
}
@ -68,20 +66,15 @@ func Validate() error {
// No problem.
case "http":
log.Warnf(
nil,
"%s was set to 'http'; this should *only* be used for debugging and tests!",
ProtocolFlag(),
)
log.Warnf(nil, "%s was set to 'http'; this should *only* be used for debugging and tests!",
ProtocolFlag)
case "":
errf("%s must be set", ProtocolFlag())
errf("%s must be set", ProtocolFlag)
default:
errf(
"%s must be set to either http or https, provided value was %s",
ProtocolFlag(), proto,
)
errf("%s must be set to either http or https, provided value was %s",
ProtocolFlag, proto)
}
// `federation-mode` should be
@ -91,22 +84,19 @@ func Validate() error {
// No problem.
case "":
errf("%s must be set", InstanceFederationModeFlag())
errf("%s must be set", InstanceFederationModeFlag)
default:
errf(
"%s must be set to either blocklist or allowlist, provided value was %s",
InstanceFederationModeFlag(), fediMode,
)
errf("%s must be set to either blocklist or allowlist, provided value was %s",
InstanceFederationModeFlag, fediMode)
}
// Parse `instance-languages`, and
// set enriched version into config.
parsedLangs, err := language.InitLangs(GetInstanceLanguages().TagStrs())
if err != nil {
errf(
"%s could not be parsed as an array of valid BCP47 language tags: %v",
InstanceLanguagesFlag(), err,
errf("%s could not be parsed as an array of valid BCP47 language tags: %v",
InstanceLanguagesFlag, err,
)
} else {
// Parsed successfully, put enriched
@ -121,37 +111,30 @@ func Validate() error {
// No problem.
default:
errf(
"%s must be set to empty string, zero, serve, or baffle, provided value was %s",
InstanceFederationModeFlag(), statsMode,
errf("%s must be set to empty string, zero, serve, or baffle, provided value was %s",
InstanceFederationModeFlag, statsMode,
)
}
// `web-assets-base-dir`.
webAssetsBaseDir := GetWebAssetBaseDir()
if webAssetsBaseDir == "" {
errf("%s must be set", WebAssetBaseDirFlag())
errf("%s must be set", WebAssetBaseDirFlag)
}
// `storage-s3-redirect-url`
if s3RedirectURL := GetStorageS3RedirectURL(); s3RedirectURL != "" {
if strings.HasSuffix(s3RedirectURL, "/") {
errf(
"%s must not end with a trailing slash",
StorageS3RedirectURLFlag(),
)
errf("%s must not end with a trailing slash",
StorageS3RedirectURLFlag)
}
if url, err := url.Parse(s3RedirectURL); err != nil {
errf(
"%s invalid: %w",
StorageS3RedirectURLFlag(), err,
)
errf("%s invalid: %w",
StorageS3RedirectURLFlag, err)
} else if url.Scheme != "https" && url.Scheme != "http" {
errf(
"%s scheme must be https or http",
StorageS3RedirectURLFlag(),
)
errf("%s scheme must be https or http",
StorageS3RedirectURLFlag)
}
}
@ -161,32 +144,42 @@ func Validate() error {
// and if using custom certs then all relevant
// values must be provided.
var (
tlsChain = GetTLSCertificateChain()
tlsKey = GetTLSCertificateKey()
tlsChainFlag = TLSCertificateChainFlag()
tlsKeyFlag = TLSCertificateKeyFlag()
tlsChain = GetTLSCertificateChain()
tlsKey = GetTLSCertificateKey()
)
if GetLetsEncryptEnabled() && (tlsChain != "" || tlsKey != "") {
errf(
"%s cannot be true when %s and/or %s are also set",
LetsEncryptEnabledFlag(), tlsChainFlag, tlsKeyFlag,
)
errf("%s cannot be true when %s and/or %s are also set",
LetsEncryptEnabledFlag, TLSCertificateChainFlag, TLSCertificateKeyFlag)
}
if (tlsChain != "" && tlsKey == "") || (tlsChain == "" && tlsKey != "") {
errf(
"%s and %s need to both be set or unset",
tlsChainFlag, tlsKeyFlag,
)
errf("%s and %s need to both be set or unset",
TLSCertificateChainFlag, TLSCertificateKeyFlag)
}
// http-client.insecure-outgoing
if GetHTTPClientInsecureOutgoing() {
log.Warn(nil, "http-client.insecure-outgoing was set to TRUE. "+
log.Warnf(nil, "%s was set to TRUE. "+
"*****THIS SHOULD BE USED FOR TESTING ONLY, IF YOU TURN THIS ON WHILE "+
"IF IN DOUBT, STOP YOUR SERVER *NOW* AND ADJUST YOUR CONFIGURATION!*****",
)
HTTPClientInsecureOutgoingFlag)
}
// thumb size recommendations,
// beyond which we log.Warn().
const minThumb = 32
const minThumbRecc = 256
const maxThumbRecc = 1024
// Get and check configured max thumb size.
switch max := GetMediaThumbMaxPixels(); {
case max < minThumb:
errf("%s < 32 is not a useable thumbsize", MediaThumbMaxPixelsFlag, max)
case max < minThumbRecc:
log.Warnf(nil, "%s smaller than min recommended thumbsize %d", MediaThumbMaxPixelsFlag, minThumbRecc)
case max > maxThumbRecc:
log.Warnf(nil, "%s larger than max recommended thumbsize %d", MediaThumbMaxPixelsFlag, maxThumbRecc)
}
return errs.Combine()

View file

@ -376,7 +376,7 @@ func sqliteConn(ctx context.Context) (*sql.DB, func() schema.Dialect, error) {
// validate db address has actually been set
address := config.GetDbAddress()
if address == "" {
return nil, nil, fmt.Errorf("'%s' was not set when attempting to start sqlite", config.DbAddressFlag())
return nil, nil, fmt.Errorf("'%s' was not set when attempting to start sqlite", config.DbAddressFlag)
}
// Build SQLite connection address with prefs.

View file

@ -65,7 +65,7 @@ func init() {
storageBasePath := config.GetStorageLocalBasePath()
if storageBasePath == "" {
return fmt.Errorf("%s must be set to do storage migration", config.StorageLocalBasePathFlag())
return fmt.Errorf("%s must be set to do storage migration", config.StorageLocalBasePathFlag)
}
return db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {

View file

@ -25,6 +25,7 @@ import (
"codeberg.org/gruf/go-kv"
"codeberg.org/gruf/go-runners"
"code.superseriousbusiness.org/gotosocial/internal/config"
"code.superseriousbusiness.org/gotosocial/internal/gtserror"
"code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
"code.superseriousbusiness.org/gotosocial/internal/log"
@ -225,6 +226,7 @@ func (p *ProcessingMedia) store(ctx context.Context) error {
if width > 0 && height > 0 {
// Determine thumbnail dimens to use.
thumbWidth, thumbHeight := thumbSize(
config.GetMediaThumbMaxPixels(),
width,
height,
aspect,

View file

@ -33,37 +33,33 @@ import (
"golang.org/x/image/webp"
)
const (
maxThumbWidth = 512
maxThumbHeight = 512
)
// thumbSize returns the dimensions to use for an input
// image of given width / height, for its outgoing thumbnail.
// This attempts to maintains the original image aspect ratio.
func thumbSize(width, height int, aspect float32) (int, int) {
func thumbSize(max, width, height int, aspect float32) (int, int) {
switch {
// Simplest case, within bounds!
case width < maxThumbWidth &&
height < maxThumbHeight:
case width < max && height < max:
return width, height
// Width is larger side.
case width > height:
// i.e. height = newWidth * (height / width)
height = int(float32(maxThumbWidth) / aspect)
return maxThumbWidth, height
height = int(float32(max) / aspect)
return max, height
// Height is larger side.
case height > width:
// i.e. width = newHeight * (width / height)
width = int(float32(maxThumbHeight) * aspect)
return width, maxThumbHeight
width = int(float32(max) * aspect)
return width, max
// Square.
default:
return maxThumbWidth, maxThumbHeight
return max, max
}
}

View file

@ -53,7 +53,7 @@ func LoadTemplates(engine *gin.Engine) error {
if templateBaseDir == "" {
return gtserror.Newf(
"%s cannot be empty and must be a relative or absolute path",
config.WebTemplateBaseDirFlag(),
config.WebTemplateBaseDirFlag,
)
}

View file

@ -326,7 +326,7 @@ func NewS3Storage() (*Driver, error) {
case "path":
bucketLookup = minio.BucketLookupPath
default:
log.Warnf(nil, "%s set to %s which is not recognized, defaulting to 'auto'", config.StorageS3BucketLookupFlag(), s)
log.Warnf(nil, "%s set to %s which is not recognized, defaulting to 'auto'", config.StorageS3BucketLookupFlag, s)
bucketLookup = minio.BucketLookupAuto
}

View file

@ -148,6 +148,7 @@ EXPECT=$(cat << "EOF"
"media-local-max-size": "420B",
"media-remote-cache-days": 30,
"media-remote-max-size": "420B",
"media-thumb-max-pixels": 42069,
"media-video-size-hint": "40.0MiB",
"metrics-enabled": false,
"oidc-admin-groups": [
@ -272,6 +273,7 @@ GTS_MEDIA_EMOJI_LOCAL_MAX_SIZE=420 \
GTS_MEDIA_EMOJI_REMOTE_MAX_SIZE=420 \
GTS_MEDIA_FFMPEG_POOL_SIZE=8 \
GTS_MEDIA_VIDEO_SIZE_HINT='40MiB' \
GTS_MEDIA_THUMB_MAX_PIXELS=42069 \
GTS_METRICS_ENABLED=false \
GTS_STORAGE_BACKEND='local' \
GTS_STORAGE_LOCAL_BASE_PATH='/root/store' \

View file

@ -114,15 +114,18 @@ func testDefaults() config.Configuration {
AccountsCustomCSSLength: 10000,
AccountsMaxProfileFields: 8,
MediaDescriptionMinChars: 0,
MediaDescriptionMaxChars: 500,
MediaRemoteCacheDays: 7,
MediaLocalMaxSize: 40 * bytesize.MiB,
MediaRemoteMaxSize: 40 * bytesize.MiB,
MediaEmojiLocalMaxSize: 51200, // 50KiB
MediaEmojiRemoteMaxSize: 102400, // 100KiB
MediaCleanupFrom: "00:00", // midnight.
MediaCleanupEvery: 24 * time.Hour, // 1/day.
Media: config.MediaConfiguration{
DescriptionMinChars: 0,
DescriptionMaxChars: 500,
RemoteCacheDays: 7,
LocalMaxSize: 40 * bytesize.MiB,
RemoteMaxSize: 40 * bytesize.MiB,
EmojiLocalMaxSize: 51200, // 50KiB
EmojiRemoteMaxSize: 102400, // 100KiB
CleanupFrom: "00:00", // midnight.
CleanupEvery: 24 * time.Hour, // 1/day.
ThumbMaxPixels: 512,
},
// the testrig only uses in-memory storage, so we can
// safely set this value to 'test' to avoid running storage