我正在尝试制作一个代理来下载具有身份验证层的服务器文件。
我正在使用 Golang (1.21.0) 和 Echo (4.11.1)
问题描述
当用户下载大文件时,如果我终止 Echo 服务器(Ctrl+C),下载只会被标记为“终止”,而不是“取消”或处于“中断错误”状态。
因此,当服务器稍后启动时,用户无法重试并恢复下载(使用范围标头)...
问题
有没有办法在停止服务器时等待每个连接以优雅地中断它而不终止用户下载?
或者,更好的是,有没有办法将下载标记为“失败”而不仅仅是“终止”?
使用的代码
package main
import (
"net/http"
"github.com/labstack/echo/v4"
)
func main() {
e := echo.New()
e.GET("/file", func(c echo.Context) error {
return c.Attachment("big_file.zip", "original file name.zip")
})
e.Logger.Fatal(e.Start(":1323"))
}
我猜您面临的问题是,当用户下载文件时停止 Echo 服务器时,下载会被标记为“终止”,而不是“取消”或处于“中断错误”状态。这可以防止用户重试下载或稍后使用范围标头恢复下载。
要解决此问题,您可以创建
http.Handler
接口的自定义实现,并在 Echo 中使用它来处理文件下载。此自定义处理程序可以跟踪活动下载并在服务器停止时优雅地中断它们。
以下是如何修改代码以实现此目的的示例:
import (
"github.com/labstack/echo/v4"
)
type downloadHandler struct {
activeDownloads map[*http.ResponseWriter]chan struct{}
mu sync.Mutex
}
func (h *downloadHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
h.mu.Lock()
defer h.mu.Unlock()
// Create a channel to track this download
done := make(chan struct{})
h.activeDownloads[&w] = done
// Remove the download channel when the response writer is closed
wrapper := &responseWriterWrapper{
ResponseWriter: w,
onClose: func() {
h.mu.Lock()
defer h.mu.Unlock()
delete(h.activeDownloads, &w)
close(done)
},
}
// Handle the file download
err := h.handleDownload(wrapper, r)
// Handle any errors during the download
if err != nil {
if errors.Is(err, os.ErrClosed) {
// The server was stopped, so the download was interrupted
wrapper.WriteHeader(http.StatusRequestTimeout)
} else {
// Other error occurred, mark the download as failed
wrapper.WriteHeader(http.StatusInternalServerError)
}
}
// Ensure the response is flushed before returning
wrapper.Flush()
}
func (h *downloadHandler) handleDownload(w http.ResponseWriter, r *http.Request) error {
// Implement your file downloading logic here
// You can use the "http.ServeFile" or any other method
filePath := "big_file.zip"
file, err := os.Open(filePath)
if err != nil {
return err
}
defer file.Close()
// Set the necessary headers for file download
w.Header().Set("Content-Disposition", "attachment; filename=original file name.zip")
w.Header().Set("Content-Type", "application/octet-stream")
// Copy the file contents to the response writer
_, err = io.Copy(w, file)
if err != nil {
return err
}
return nil
}
type responseWriterWrapper struct {
http.ResponseWriter
onClose func()
}
func (w *responseWriterWrapper) CloseNotify() <-chan bool {
return w.ResponseWriter.(http.CloseNotifier).CloseNotify()
}
func (w *responseWriterWrapper) CloseNotifyChan() chan struct{} {
c := make(chan struct{})
go func() {
select {
case <-w.ResponseWriter.(http.CloseNotifier).CloseNotify():
case <-c:
}
w.onClose()
}()
return c
}
func main() {
// Set up a signal channel to listen for server stop signal
stop := make(chan os.Signal, 1)
signal.Notify(stop, os.Interrupt)
// Initialize the download handler and map to store active downloads
handler := &downloadHandler{
activeDownloads: make(map[*http.ResponseWriter]chan struct{}),
}
// Create a new Echo server
e := echo.New()
// Register the custom download handler
e.GET("/file", echo.WrapHandler(handler))
// Start the server in a goroutine
go func() {
if err := e.Start(":1323"); err != nil {
e.Logger.Fatal(err)
}
}()
// Wait for the stop signal
<-stop
// Stop the server gracefully
if err := e.Shutdown(); err != nil {
e.Logger.Fatal(err)
}
// Notify all active downloads about the interruption
handler.mu.Lock()
defer handler.mu.Unlock()
for _, done := range handler.activeDownloads {
close(done)
}
}
在此修改后的代码中,实现了自定义
downloadHandler
结构来处理文件下载。它在地图中跟踪活动下载,其中键是指向 http.ResponseWriter
的指针,值是用于跟踪下载状态的通道 (done
)。
当收到请求时,会创建一个新的
done
通道并将其添加到地图中。然后,请求和响应将传递到 handleDownload
方法,您可以在其中实现文件下载逻辑。
responseWriterWrapper
包装了原始的http.ResponseWriter
以拦截CloseNotify
方法并在响应编写器关闭时调用onClose
回调。这使我们能够从地图上删除下载通道并优雅地中断下载。
当服务器收到停止信号(例如 Ctrl+C)时,它会使用
e.Shutdown()
正常关闭。在退出之前,它会通过关闭各自的 done
通道来通知所有活动下载有关中断的信息。
此实现可确保在服务器停止时正常中断下载,从而允许用户稍后重试或恢复下载。
有点长,抱歉。