{"record":{"id":"d93a2a14b30c5eb2","repo":"usememos/memos","slug":"attachment-storage-is-not-configured","errorCode":null,"errorMessage":"attachment storage is not configured","messagePattern":"attachment storage is not configured","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"store/storage.go","lineNumber":246,"sourceCode":"\t\ts3Config = setting.GetS3Config()\n\t}\n\tif s3Config != nil {\n\t\t// Prefer the configured storage addressing the same namespace so legacy\n\t\t// attachments pick up rotated credentials and transport options.\n\t\tlegacyStorage := &storepb.Storage{\n\t\t\tType:   storepb.StorageType_STORAGE_TYPE_S3,\n\t\t\tConfig: &storepb.Storage_S3Config{S3Config: s3Config},\n\t\t}\n\t\tif configuredStorage := findEquivalentStorage(setting.GetStorages(), legacyStorage); configuredStorage != nil {\n\t\t\treturn configuredStorage, nil\n\t\t}\n\t\treturn legacyStorage, nil\n\t}\n\n\tif storageID != \"\" {\n\t\treturn nil, errors.Errorf(\"storage %q is not configured\", storageID)\n\t}\n\treturn nil, errors.New(\"attachment storage is not configured\")\n}\n\n// ResolveStorageDriver resolves the configured storage referenced by an attachment\n// and returns its driver. legacyS3Config supports attachments written before\n// storage IDs were introduced.\nfunc ResolveStorageDriver(\n\tctx context.Context,\n\tsetting *storepb.InstanceStorageSetting,\n\tstorageID string,\n\tlegacyS3Config *storepb.StorageS3Config,\n) (storage.Driver, error) {\n\tresolvedStorage, err := ResolveStorage(setting, storageID, legacyS3Config)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn storage.NewDriver(ctx, resolvedStorage)\n}\n","sourceCodeStart":228,"sourceCodeEnd":264,"githubUrl":"https://github.com/usememos/memos/blob/14d757ce1fb31c78590f374bc042f8dbedbc20d7/store/storage.go#L228-L264","documentation":"ResolveStorage (store/storage.go:212) walks a fallback chain to find the storage backend for an attachment: explicit storageID lookup in the instance setting, then the attachment's legacy S3 config, then the instance-level legacy S3 config. Reaching line 246 means storageID was empty AND no S3 config existed anywhere, so no driver can serve the attachment — errors.New(\"attachment storage is not configured\"). Contrast with line 244, which fires when a non-empty storageID is simply absent from the setting.","triggerScenarios":"Reading/downloading an attachment that has no storage_id and no legacy S3 config (i.e. it was expected to be LOCAL) while the instance storage setting contains no usable local entry; the instance setting was wiped, replaced by a config-file deployment, or restored from a backup that dropped storage configuration.","commonSituations":"Migrating a database between instances without copying workspace storage settings; rolling back to a config-file-based S3 setup after using the storage registry; a fresh install pointing at an old database; manually editing the workspace_setting row and losing the storages list.","solutions":["Configure an instance storage setting with at least one storage (a LOCAL type entry is enough) via the workspace setting API or admin UI, so attachments without an explicit storage ID resolve to the default.","If the attachments were originally S3-backed, restore the legacy S3 config (workspace-level S3 setting) or add an S3 storage entry addressing the same bucket so findEquivalentStorage matches it and rotated credentials are picked up.","Check the attachment row: if it should point at a specific storage, set its storage_id to a configured storage ID.","Restore the workspace setting from a backup taken before the configuration was lost rather than re-creating storages with new IDs."],"exampleFix":"// before: instance setting has no storages, attachment has no storage_id\nstor, err := store.ResolveStorage(setting, att.StorageId, att.S3Config) // error\n\n// after: ensure a default LOCAL storage exists before resolving\nstore.NormalizeInstanceStorageSetting(setting) // appends LOCAL fallback if empty\nstor, err := store.ResolveStorage(setting, att.StorageId, att.S3Config)","handlingStrategy":"fallback","validationCode":"// Before resolving an attachment's driver, ensure the instance can serve it:\nfunc canResolveStorage(setting *storepb.InstanceStorageSetting, att *storepb.Attachment) bool {\n    if att.GetStorageId() != \"\" && store.FindStorage(setting, att.GetStorageId()) != nil {\n        return true\n    }\n    return att.GetS3Config() != nil || setting.GetS3Config() != nil || len(setting.GetStorages()) > 0\n}","typeGuard":null,"tryCatchPattern":"stor, err := store.ResolveStorage(setting, att.StorageId, att.S3Config)\nif err != nil {\n    if strings.Contains(err.Error(), \"storage is not configured\") {\n        // surface a configuration action to the admin: set up an instance\n        // storage (LOCAL or S3) before attachments can be served\n    }\n    return err\n}","preventionTips":["Back up the workspace storage setting together with the database before migrations or instance moves.","Keep at least one LOCAL storage entry in the instance setting as a permanent fallback.","When re-creating S3 storages, reuse the original bucket/endpoint so findEquivalentStorage matches legacy attachments.","Set explicit storage IDs on attachments when migrating off the legacy S3 config."],"tags":["storage","attachments","s3","configuration"],"backgroundTag":null,"analyzedSha":"14d757ce1fb31c78590f374bc042f8dbedbc20d7","analyzedAt":"2026-08-15T09:27:36.538Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}