diff --git a/docs/使用指南.md b/docs/使用指南.md index 70caa3e..18c8a4f 100644 --- a/docs/使用指南.md +++ b/docs/使用指南.md @@ -118,12 +118,14 @@ Halo 默认的附件可以通过 `/upload/文件名` 直链被任何人直接下 | **资源名称** * | 显示给访客的名称,也是下载文件的文件名(无扩展名时自动补上附件原扩展名) | | **slug** * | 下载链接的标识,如填 `whitepaper-2024`,下载链接就是 `/download/whitepaper-2024`。仅限小写字母、数字、中划线。**创建后不可修改** | | **描述** | 可选,显示在下载页标题下方 | -| **附件** | 点击「选择附件」从附件库选择(也可先上传新附件) | +| **附件** | 点击「选择附件」从附件库选择(也可先上传新附件);支持「外部链接」类型的附件(见下方说明) | | **需要邮箱验证** | 开启后访客必须验证邮箱才能下载 | | **启用** | 停用后下载页和下载链接立即 404,但配置保留 | 保存后回到列表,点击该行的 **「复制」** 按钮即可拿到完整下载链接(含域名),粘贴到文章的任意位置(普通链接、按钮、卡片都可以)。 +> 💡 **外部链接附件**:如果选择的是「外部链接」类型的附件(文件不在本站存储),访客通过验证、计数完成后,浏览器会被 302 跳转到该外部地址直接下载——文件不经服务器中转,不占带宽;但真实外部地址会暴露给访客,且防直链不适用(详见第十节)。 + ### 4.2 列表各列含义 - **邮箱验证**:该资源是否需要验证(需要 / 不需要) @@ -268,6 +270,7 @@ Halo 默认的附件可以通过 `/upload/文件名` 直链被任何人直接下 |---|---| | 本地存储策略的附件(你当前的使用方式) | ✅ 直链 404 | | 外部对象存储(S3/OSS 等)的附件 | ❌ 无法拦截(文件不经 Halo 发出),但下载统计和邮箱验证仍正常 | +| 外部链接类型的附件 | ❌ 不适用;下载时验证通过后 302 跳转到外部地址,真实地址对访客可见 | | 同一附件被文章当图片直接引用 | ⚠️ 会被一并拦截,图片变 404! | > ⚠️ **最重要的使用纪律**:不要把文章中需要**直接显示**的图片/附件注册为下载资源。注册即等于"此文件只能经下载页获取"。 @@ -302,3 +305,6 @@ A:正常。同一个人多次下载,次数累加、人数去重。 **Q:对象存储附件能防直链吗?** A:不能。防直链只对本地存储策略生效;但统计与邮箱验证对所有存储策略都有效。 + +**Q:资源绑的是「外部链接」附件,下载是什么行为?** +A:访客点击下载、(如需)完成邮箱验证并计数后,浏览器直接 302 跳转到外部地址下载,文件不经服务器中转。外部链接失效时跳转会由目标站点报错(不再是插件的 404 页)。 diff --git a/gradle.properties b/gradle.properties index cc6016e..1de8cfc 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1 +1 @@ -version=1.3.2 +version=1.3.3 diff --git a/settings.gradle b/settings.gradle index 4f78965..e6b6534 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,5 +1,5 @@ plugins { - id 'org.gradle.toolchains.foojay-resolver-convention' version '0.8.0' + id 'org.gradle.toolchains.foojay-resolver-convention' version '1.0.0' } rootProject.name = 'sharelink' diff --git a/src/main/java/io/github/shirainbown/sharelink/download/FileStreamer.java b/src/main/java/io/github/shirainbown/sharelink/download/FileStreamer.java index 4a8b91a..fcedb46 100644 --- a/src/main/java/io/github/shirainbown/sharelink/download/FileStreamer.java +++ b/src/main/java/io/github/shirainbown/sharelink/download/FileStreamer.java @@ -35,10 +35,12 @@ import io.github.shirainbown.sharelink.model.DownloadResource; * Streams the attachment bytes of a download resource to the visitor. * *
For the local storage policy the file is read directly from disk - * ({work-dir}/attachments/{local-relative-path}); for other policies the file is fetched - * through a loopback HTTP request to its permalink (carrying the internal secret header - * so {@code UploadProtectFilter} lets it pass). The body is forwarded as a - * {@link DataBuffer} stream without buffering the whole file in memory. + * ({work-dir}/attachments/{local-relative-path}); for external-link attachments (the + * permalink is an absolute http(s) URL) the visitor is redirected (302) to the external + * URL after the token has been consumed and the download recorded; for other policies + * the file is fetched through a loopback HTTP request to its permalink (carrying the + * internal secret header so {@code UploadProtectFilter} lets it pass). The body is + * forwarded as a {@link DataBuffer} stream without buffering the whole file in memory. */ @Slf4j @Component @@ -156,6 +158,19 @@ public class FileStreamer { if (StringUtils.isBlank(permalink)) { return errorPage(HttpStatus.NOT_FOUND, "附件暂不可用"); } + // External-link attachment: the permalink is already an absolute URL. Hand the + // download to the browser instead of proxy-fetching it server-side (the proxy + // breaks on expired links, anti-hotlink 403s and un-followed 301/302s). + if (StringUtils.startsWithAny(permalink, "http://", "https://")) { + try { + return ServerResponse.status(HttpStatus.FOUND) + .location(URI.create(permalink)) + .cacheControl(CacheControl.noStore()) + .build(); + } catch (IllegalArgumentException e) { + log.warn("外链附件地址非法,回退为代理抓取: {}", permalink, e); + } + } final URI fileUri; try { fileUri = externalUrlSupplier.getURL(request.exchange().getRequest())