feat: secure storage artifact preview

This commit is contained in:
www
2026-04-19 21:29:51 +08:00
parent 566e966582
commit 2b2b822631
4 changed files with 57 additions and 9 deletions

View File

@@ -22,18 +22,19 @@ Route::group("/guest", function () {
# 系统配置
Route::group("/system", function () {
Route::get("/user/info", [AdminUser::class, "info"])->name("AdminUser@info");
Route::get("/user/list", [AdminUser::class, "getAdminUserList"])->name("AdminUser@getAdminUserList");
Route::post("/user/save", [AdminUser::class, "saveAdminUser"])->name("AdminUser@saveAdminUser");
Route::post("/user/del", [AdminUser::class, "delAdminUser"])->name("AdminUser@delAdminUser");
Route::group("/system", function () {
Route::get("/user/info", [AdminUser::class, "info"])->name("AdminUser@info");
Route::get("/user/list", [AdminUser::class, "getAdminUserList"])->name("AdminUser@getAdminUserList");
Route::post("/user/save", [AdminUser::class, "saveAdminUser"])->name("AdminUser@saveAdminUser");
Route::post("/user/del", [AdminUser::class, "delAdminUser"])->name("AdminUser@delAdminUser");
Route::get("/config/list", [SystemConfig::class, "getSystemConfigList"])->name("SystemConfig@getSystemConfigList");
Route::post("/config/save", [SystemConfig::class, "saveSystemConfig"])->name("SystemConfig@saveSystemConfig");
Route::post("/config/seo-ai-rules/reset", [SystemConfig::class, "resetSeoAiRules"])->name("SystemConfig@resetSeoAiRules");
Route::get("/cjpz/list", [SystemConfig::class, "getCaiJiPeiZhiList"])->name("SystemConfig@getCaiJiPeiZhiList");
Route::post("/cjpz/save", [SystemConfig::class, "saveCaiJiPeiZhi"])->name("SystemConfig@saveCaiJiPeiZhi");
Route::get("/storage/file", [SystemConfig::class, "previewStorageFile"])->name("SystemConfig@previewStorageFile");
Route::get("/cjpz/list", [SystemConfig::class, "getCaiJiPeiZhiList"])->name("SystemConfig@getCaiJiPeiZhiList");
Route::post("/cjpz/save", [SystemConfig::class, "saveCaiJiPeiZhi"])->name("SystemConfig@saveCaiJiPeiZhi");
Route::get("/plan/list", [SystemConfig::class, "getPlanTaskList"])->name("SystemConfig@getPlanTaskList");
Route::post("/plan/save", [SystemConfig::class, "savePlanTask"])->name("SystemConfig@savePlanTask");

View File

@@ -25,6 +25,44 @@ use think\Response;
class SystemConfig extends BaseController
{
public function previewStorageFile(Request $Request, ?AdminUserModel $AdminUserModel)
{
$strRelativePath = trim(str_replace('\\', '/', (string)$Request->get('path', '')));
if ($strRelativePath === '') {
return response('storage path is required', 400, ['Content-Type' => 'text/plain; charset=UTF-8']);
}
$strRelativePath = ltrim($strRelativePath, '/');
if (str_starts_with($strRelativePath, 'storage/')) {
$strRelativePath = substr($strRelativePath, strlen('storage/'));
}
$strStorageRoot = rtrim(str_replace('\\', '/', (string)root_path()), '/') . '/storage';
$strStorageRootReal = realpath($strStorageRoot);
$strCandidatePath = $strStorageRoot . '/' . $strRelativePath;
$strCandidateReal = realpath($strCandidatePath);
if ($strStorageRootReal === false || $strCandidateReal === false || !is_file($strCandidateReal)) {
return response('storage file not found', 404, ['Content-Type' => 'text/plain; charset=UTF-8']);
}
$strStorageRootReal = str_replace('\\', '/', $strStorageRootReal);
$strCandidateReal = str_replace('\\', '/', $strCandidateReal);
if (!str_starts_with($strCandidateReal, $strStorageRootReal . '/')) {
return response('storage file is out of allowed root', 403, ['Content-Type' => 'text/plain; charset=UTF-8']);
}
$strMimeType = (string)(mime_content_type($strCandidateReal) ?: 'application/octet-stream');
$strFilename = basename($strCandidateReal);
$strContent = (string)file_get_contents($strCandidateReal);
return response($strContent, 200, [
'Content-Type' => $strMimeType,
'Content-Disposition' => 'inline; filename="' . addslashes($strFilename) . '"',
'X-Storage-Relative-Path' => $strRelativePath,
]);
}
/**
* 采集配置列表

View File

@@ -83,6 +83,7 @@
3. `video-metadata-missing-task-pool` 已迁到 `storage/video-metadata-missing/task-pool`
4. `domain-seo-external/manual-import` 已迁到 `storage/external-seo/manual-import`
5. `domain-seo-external/search-console` 模板目录已迁到 `storage/external-seo/search-console`
6. `Vue 后台` 打开 `external-seo/*``video-metadata-missing/*` 这类产物时,不再假设文件直接暴露在 `public`,而是通过后台鉴权预览入口读取 `storage` 文件
以后如果再新增类似后台产物目录,优先原则是:
@@ -90,6 +91,13 @@
2. 默认不要放 `public`
3. 只有明确需要前台公开访问的静态资源,才考虑进入 `public`
补充一条执行口径:
1. 如果后台页面需要“打开 HTML / JSON / CSV / prompt / payload”
2. 优先保留文件在 `storage`
3. 不要因为前台要看,就把私有运行产物重新搬回 `public`
4. 应优先补 `后台鉴权读取接口 + Vue 打开工具函数`
### 3. 公共规则侧
已确立为全项目公共主线的内容:

View File

@@ -57,6 +57,7 @@
3. 需要确认后台真实页面入口现在在 Vue而不是 PHP 直出页
4. 需要同步今天补上的 `Spider MD 异步任务消费器``GitHub 推送``主表一键复制MD` 这些后台能力
5. 需要同步“所有后台运行产物、工作台快照、手工导入中间文件默认落 `storage`,不要再放 `public`”这条新口径
6. 需要同步“Vue 后台打开 `storage` 产物时,走后台鉴权预览入口,不要再直接拼 `public` URL”这条新口径
### 1.3 AI文案与视频元数据公共规则