feat: support STORAGE_PATH_PREFIX for global storage key namespacing

Add STORAGE_PATH_PREFIX config field and apply prefix in Storage wrapper
for all operations (save, load, download, exists, delete, scan).
Scan strips prefix from results to prevent double-prefixing.
Aliyun OSS disables its own folder prefix when STORAGE_PATH_PREFIX is set.
This commit is contained in:
GareArc
2026-04-16 02:25:47 -07:00
parent 7f4bf19186
commit 62d25cbffb
3 changed files with 27 additions and 8 deletions

View File

@ -74,6 +74,11 @@ class StorageConfig(BaseSettings):
default="opendal",
)
STORAGE_PATH_PREFIX: str = Field(
description="Global path prefix prepended to all storage object keys.",
default="",
)
STORAGE_LOCAL_PATH: str = Field(
description="Path for local storage when STORAGE_TYPE is set to 'local'.",
default="storage",

View File

@ -1,4 +1,5 @@
import logging
import posixpath
from collections.abc import Callable, Generator
from typing import Literal, Union, overload
@ -17,6 +18,15 @@ class Storage:
storage_factory = self.get_storage_factory(dify_config.STORAGE_TYPE)
with app.app_context():
self.storage_runner = storage_factory()
prefix = dify_config.STORAGE_PATH_PREFIX.strip("/") if dify_config.STORAGE_PATH_PREFIX else ""
if prefix and ".." in prefix.split("/"):
raise ValueError(f"STORAGE_PATH_PREFIX must not contain '..': {dify_config.STORAGE_PATH_PREFIX}")
self._path_prefix = prefix
def _prefix(self, filename: str) -> str:
if not self._path_prefix:
return filename
return posixpath.join(self._path_prefix, filename)
@staticmethod
def get_storage_factory(storage_type: str) -> Callable[[], BaseStorage]:
@ -86,7 +96,7 @@ class Storage:
raise ValueError(f"unsupported storage type {storage_type}")
def save(self, filename: str, data: bytes):
self.storage_runner.save(filename, data)
self.storage_runner.save(self._prefix(filename), data)
@overload
def load(self, filename: str, /, *, stream: Literal[False] = False) -> bytes: ...
@ -105,22 +115,26 @@ class Storage:
return self.load_once(filename)
def load_once(self, filename: str) -> bytes:
return self.storage_runner.load_once(filename)
return self.storage_runner.load_once(self._prefix(filename))
def load_stream(self, filename: str) -> Generator:
return self.storage_runner.load_stream(filename)
return self.storage_runner.load_stream(self._prefix(filename))
def download(self, filename, target_filepath):
self.storage_runner.download(filename, target_filepath)
self.storage_runner.download(self._prefix(filename), target_filepath)
def exists(self, filename):
return self.storage_runner.exists(filename)
return self.storage_runner.exists(self._prefix(filename))
def delete(self, filename: str):
return self.storage_runner.delete(filename)
return self.storage_runner.delete(self._prefix(filename))
def scan(self, path: str, files: bool = True, directories: bool = False) -> list[str]:
return self.storage_runner.scan(path, files=files, directories=directories)
results = self.storage_runner.scan(self._prefix(path), files=files, directories=directories)
if not self._path_prefix:
return results
prefix_with_slash = self._path_prefix + "/"
return [r[len(prefix_with_slash):] if r.startswith(prefix_with_slash) else r for r in results]
storage = Storage()

View File

@ -13,7 +13,7 @@ class AliyunOssStorage(BaseStorage):
def __init__(self):
super().__init__()
self.bucket_name = dify_config.ALIYUN_OSS_BUCKET_NAME
self.folder = dify_config.ALIYUN_OSS_PATH
self.folder = None if dify_config.STORAGE_PATH_PREFIX else dify_config.ALIYUN_OSS_PATH
oss_auth_method = aliyun_s3.Auth
region = None
if dify_config.ALIYUN_OSS_AUTH_VERSION == "v4":