diff --git a/docs/swagger/docs.go b/docs/swagger/docs.go index 5018806e..22563b8d 100644 --- a/docs/swagger/docs.go +++ b/docs/swagger/docs.go @@ -439,13 +439,8 @@ const docTemplate = `{ "type": "object", "properties": { "expires": { - "description": "Deprecated, used only for legacy APIs", "type": "integer" }, - "session": { - "description": "Deprecated, used only for legacy APIs", - "type": "string" - }, "token": { "type": "string" } diff --git a/docs/swagger/swagger.json b/docs/swagger/swagger.json index 6c37347b..cfaf818b 100644 --- a/docs/swagger/swagger.json +++ b/docs/swagger/swagger.json @@ -428,13 +428,8 @@ "type": "object", "properties": { "expires": { - "description": "Deprecated, used only for legacy APIs", "type": "integer" }, - "session": { - "description": "Deprecated, used only for legacy APIs", - "type": "string" - }, "token": { "type": "string" } diff --git a/docs/swagger/swagger.yaml b/docs/swagger/swagger.yaml index 4c95a948..c9d48d4e 100644 --- a/docs/swagger/swagger.yaml +++ b/docs/swagger/swagger.yaml @@ -27,11 +27,7 @@ definitions: api_v1.loginResponseMessage: properties: expires: - description: Deprecated, used only for legacy APIs type: integer - session: - description: Deprecated, used only for legacy APIs - type: string token: type: string type: object diff --git a/internal/http/handlers/api/v1/accounts_test.go b/internal/http/handlers/api/v1/accounts_test.go index 9dbb89c3..081b9ff9 100644 --- a/internal/http/handlers/api/v1/accounts_test.go +++ b/internal/http/handlers/api/v1/accounts_test.go @@ -357,9 +357,7 @@ func TestHandleUpdateAccount(t *testing.T) { // Verify we can login with new password loginBody := `{"username": "shiori", "password": "newpass"}` - w = testutil.PerformRequest(deps, func(deps model.Dependencies, c model.WebContext) { - HandleLogin(deps, c, noopLegacyLoginHandler) - }, "POST", "/login", testutil.WithBody(loginBody)) + w = testutil.PerformRequest(deps, HandleLogin, "POST", "/login", testutil.WithBody(loginBody)) require.Equal(t, http.StatusOK, w.Code) }) @@ -480,9 +478,7 @@ func TestHandleUpdateAccount(t *testing.T) { // Verify password change loginBody := `{"username": "updated", "password": "newpass"}` - w = testutil.PerformRequest(deps, func(deps model.Dependencies, c model.WebContext) { - HandleLogin(deps, c, noopLegacyLoginHandler) - }, "POST", "/login", testutil.WithBody(loginBody)) + w = testutil.PerformRequest(deps, HandleLogin, "POST", "/login", testutil.WithBody(loginBody)) require.Equal(t, http.StatusOK, w.Code) }) } diff --git a/internal/http/handlers/api/v1/auth.go b/internal/http/handlers/api/v1/auth.go index 717ffda0..eab98960 100644 --- a/internal/http/handlers/api/v1/auth.go +++ b/internal/http/handlers/api/v1/auth.go @@ -29,8 +29,7 @@ func (p *loginRequestPayload) IsValid() error { type loginResponseMessage struct { Token string `json:"token"` - SessionID string `json:"session"` // Deprecated, used only for legacy APIs - Expiration int64 `json:"expires"` // Deprecated, used only for legacy APIs + Expiration int64 `json:"expires"` } // @Summary Login to an account using username and password @@ -41,7 +40,7 @@ type loginResponseMessage struct { // @Success 200 {object} loginResponseMessage "Login successful" // @Failure 400 {object} nil "Invalid login data" // @Router /api/v1/auth/login [post] -func HandleLogin(deps model.Dependencies, c model.WebContext, legacyLoginHandler model.LegacyLoginHandler) { +func HandleLogin(deps model.Dependencies, c model.WebContext) { var payload loginRequestPayload if err := json.NewDecoder(c.Request().Body).Decode(&payload); err != nil { response.SendError(c, http.StatusBadRequest, "Invalid JSON payload", nil) @@ -72,16 +71,8 @@ func HandleLogin(deps model.Dependencies, c model.WebContext, legacyLoginHandler return } - sessionID, err := legacyLoginHandler(account, expiration) - if err != nil { - deps.Logger().WithError(err).Error("failed execute legacy login handler") - response.SendInternalServerError(c) - return - } - response.Send(c, http.StatusOK, loginResponseMessage{ Token: token, - SessionID: sessionID, Expiration: expirationTime.Unix(), }) } @@ -215,5 +206,12 @@ func HandleLogout(deps model.Dependencies, c model.WebContext) { if err := middleware.RequireLoggedInUser(deps, c); err != nil { return } + + // Remove token cookie + c.Request().AddCookie(&http.Cookie{ + Name: "token", + Value: "", + }) + response.Send(c, http.StatusOK, nil) } diff --git a/internal/http/handlers/api/v1/auth_test.go b/internal/http/handlers/api/v1/auth_test.go index b7848211..59aa4897 100644 --- a/internal/http/handlers/api/v1/auth_test.go +++ b/internal/http/handlers/api/v1/auth_test.go @@ -4,7 +4,6 @@ import ( "context" "net/http" "testing" - "time" "github.com/go-shiori/shiori/internal/model" "github.com/go-shiori/shiori/internal/testutil" @@ -12,10 +11,6 @@ import ( "github.com/stretchr/testify/require" ) -func noopLegacyLoginHandler(_ *model.AccountDTO, _ time.Duration) (string, error) { - return "test-session", nil -} - func TestHandleLogin(t *testing.T) { logger := logrus.New() // _, deps := testutil.GetTestConfigurationAndDependencies(t, context.Background(), logger) @@ -24,9 +19,7 @@ func TestHandleLogin(t *testing.T) { ctx := context.Background() _, deps := testutil.GetTestConfigurationAndDependencies(t, ctx, logger) body := `{"username":}` - w := testutil.PerformRequest(deps, func(deps model.Dependencies, c model.WebContext) { - HandleLogin(deps, c, noopLegacyLoginHandler) - }, "POST", "/login", testutil.WithBody(body)) + w := testutil.PerformRequest(deps, HandleLogin, "POST", "/login", testutil.WithBody(body)) require.Equal(t, http.StatusBadRequest, w.Code) }) @@ -34,9 +27,7 @@ func TestHandleLogin(t *testing.T) { ctx := context.Background() _, deps := testutil.GetTestConfigurationAndDependencies(t, ctx, logger) body := `{"password": "test"}` - w := testutil.PerformRequest(deps, func(deps model.Dependencies, c model.WebContext) { - HandleLogin(deps, c, noopLegacyLoginHandler) - }, "POST", "/login", testutil.WithBody(body)) + w := testutil.PerformRequest(deps, HandleLogin, "POST", "/login", testutil.WithBody(body)) require.Equal(t, http.StatusBadRequest, w.Code) }) @@ -44,9 +35,7 @@ func TestHandleLogin(t *testing.T) { ctx := context.Background() _, deps := testutil.GetTestConfigurationAndDependencies(t, ctx, logger) body := `{"username": "test"}` - w := testutil.PerformRequest(deps, func(deps model.Dependencies, c model.WebContext) { - HandleLogin(deps, c, noopLegacyLoginHandler) - }, "POST", "/login", testutil.WithBody(body)) + w := testutil.PerformRequest(deps, HandleLogin, "POST", "/login", testutil.WithBody(body)) require.Equal(t, http.StatusBadRequest, w.Code) }) @@ -54,9 +43,7 @@ func TestHandleLogin(t *testing.T) { ctx := context.Background() _, deps := testutil.GetTestConfigurationAndDependencies(t, ctx, logger) body := `{"username": "test", "password": "wrong"}` - w := testutil.PerformRequest(deps, func(deps model.Dependencies, c model.WebContext) { - HandleLogin(deps, c, noopLegacyLoginHandler) - }, "POST", "/login", testutil.WithBody(body)) + w := testutil.PerformRequest(deps, HandleLogin, "POST", "/login", testutil.WithBody(body)) require.Equal(t, http.StatusBadRequest, w.Code) }) @@ -74,16 +61,13 @@ func TestHandleLogin(t *testing.T) { "password": "test", "remember_me": true }` - w := testutil.PerformRequest(deps, func(deps model.Dependencies, c model.WebContext) { - HandleLogin(deps, c, noopLegacyLoginHandler) - }, "POST", "/login", testutil.WithBody(body)) + w := testutil.PerformRequest(deps, HandleLogin, "POST", "/login", testutil.WithBody(body)) require.Equal(t, http.StatusOK, w.Code) response, err := testutil.NewTestResponseFromReader(w.Body) require.NoError(t, err) response.AssertOk(t) response.AssertMessageContains(t, "token") - response.AssertMessageContains(t, "session") response.AssertMessageContains(t, "expires") }) } diff --git a/internal/http/handlers/bookmark.go b/internal/http/handlers/bookmark.go index 0720b279..4bca19b3 100644 --- a/internal/http/handlers/bookmark.go +++ b/internal/http/handlers/bookmark.go @@ -5,7 +5,6 @@ import ( "html/template" "net/http" "strconv" - "strings" "github.com/go-shiori/shiori/internal/http/response" "github.com/go-shiori/shiori/internal/model" @@ -92,8 +91,7 @@ func HandleBookmarkArchiveFile(deps model.Dependencies, c model.WebContext) { return } - // Get resource path from URL - resourcePath := strings.TrimPrefix(c.Request().URL.Path, fmt.Sprintf("/bookmark/%d/archive/file/", bookmark.ID)) + resourcePath := c.Request().PathValue("path") archive, err := deps.Domains().Archiver().GetBookmarkArchive(bookmark) if err != nil { diff --git a/internal/http/handlers/legacy.go b/internal/http/handlers/legacy.go index 70ffa0ca..9af42374 100644 --- a/internal/http/handlers/legacy.go +++ b/internal/http/handlers/legacy.go @@ -50,15 +50,18 @@ func (h *LegacyHandler) HandleLogin(account *model.AccountDTO, expTime time.Dura } strSessionID := sessionID.String() - h.legacyHandler.SessionCache.Set(strSessionID, account, expTime) return strSessionID, nil } // HandleLogout handles the legacy logout endpoint func (h *LegacyHandler) HandleLogout(deps model.Dependencies, c model.WebContext) { - sessionID := h.legacyHandler.GetSessionID(c.Request()) - h.legacyHandler.SessionCache.Delete(sessionID) + // TODO: Leave cookie handling to API consumer or middleware? + // Remove token cookie + c.Request().AddCookie(&http.Cookie{ + Name: "token", + Value: "", + }) } // HandleGetTags handles GET /api/tags diff --git a/internal/http/handlers/legacy_test.go b/internal/http/handlers/legacy_test.go index 850c02e1..a7198a1f 100644 --- a/internal/http/handlers/legacy_test.go +++ b/internal/http/handlers/legacy_test.go @@ -36,30 +36,6 @@ func TestLegacyHandler(t *testing.T) { sessionID, err := handler.HandleLogin(account, time.Hour) require.NoError(t, err) require.NotEmpty(t, sessionID) - - // Verify session is stored - val, found := handler.legacyHandler.SessionCache.Get(sessionID) - require.True(t, found) - require.Equal(t, account, val) - }) - - t.Run("HandleLogout", func(t *testing.T) { - // Setup session - account := &model.AccountDTO{ID: 1} - sessionID, _ := handler.HandleLogin(account, time.Hour) - - // Create request with session cookie - c, _ := testutil.NewTestWebContext() - c.Request().AddCookie(&http.Cookie{ - Name: "session-id", - Value: sessionID, - }) - - handler.HandleLogout(deps, c) - - // Verify session is removed - _, found := handler.legacyHandler.SessionCache.Get(sessionID) - require.False(t, found) }) t.Run("HandleGetTags", func(t *testing.T) { @@ -79,7 +55,7 @@ func TestLegacyHandler(t *testing.T) { }) t.Run("convertParams", func(t *testing.T) { - r, _ := http.NewRequest(http.MethodGet, "/api/bookmarks?page=1&tags=test,dev", nil) + r, _ := http.NewRequest(http.MethodGet, "/api/bookmarks?page=1&tags=test,dev", http.NoBody) params := handler.convertParams(r) require.Len(t, params, 2) diff --git a/internal/http/middleware/auth.go b/internal/http/middleware/auth.go index ba494418..8307a772 100644 --- a/internal/http/middleware/auth.go +++ b/internal/http/middleware/auth.go @@ -32,8 +32,14 @@ func (m *AuthMiddleware) OnRequest(deps model.Dependencies, c model.WebContext) account, err := deps.Domains().Auth().CheckToken(c.Request().Context(), token) if err != nil { - deps.Logger().WithError(err).Error("Failed to check token") - return err + // If we fail to check token, remove the token cookie and redirect to login + deps.Logger().WithError(err).WithField("request_id", c.GetRequestID()).Error("Failed to check token") + http.SetCookie(c.ResponseWriter(), &http.Cookie{ + Name: "token", + Value: "", + MaxAge: -1, + }) + return nil } c.SetAccount(account) diff --git a/internal/http/middleware/auth_test.go b/internal/http/middleware/auth_test.go index 006622c1..2b6a2cb9 100644 --- a/internal/http/middleware/auth_test.go +++ b/internal/http/middleware/auth_test.go @@ -64,6 +64,39 @@ func TestAuthMiddleware(t *testing.T) { require.NoError(t, err) require.NotNil(t, c.GetAccount()) }) + + t.Run("test invalid token cookie is removed", func(t *testing.T) { + // Create an invalid token + invalidToken := "invalid-token" + + w := httptest.NewRecorder() + r := httptest.NewRequest(http.MethodGet, "/", nil) + r.AddCookie(&http.Cookie{ + Name: "token", + Value: invalidToken, + MaxAge: int(time.Now().Add(time.Minute).Unix()), + }) + c := webcontext.NewWebContext(w, r) + + middleware := NewAuthMiddleware(deps) + err := middleware.OnRequest(deps, c) + require.NoError(t, err) + require.Nil(t, c.GetAccount()) + + // Check that the token cookie was removed in the response + responseCookies := w.Result().Cookies() + + var tokenCookie *http.Cookie + for _, cookie := range responseCookies { + if cookie.Name == "token" { + tokenCookie = cookie + break + } + } + + require.NotNil(t, tokenCookie, "Token cookie should exist in response") + require.Empty(t, tokenCookie.Value, "Token cookie value should be empty") + }) } func TestRequireLoggedInUser(t *testing.T) { diff --git a/internal/http/server.go b/internal/http/server.go index f50befb2..42aca9a8 100644 --- a/internal/http/server.go +++ b/internal/http/server.go @@ -49,7 +49,7 @@ func (s *HttpServer) Setup(cfg *config.Config, deps *dependencies.Dependencies) // Bookmark routes s.mux.HandleFunc("GET /bookmark/{id}/content", ToHTTPHandler(deps, handlers.HandleBookmarkContent, globalMiddleware...)) s.mux.HandleFunc("GET /bookmark/{id}/archive", ToHTTPHandler(deps, handlers.HandleBookmarkArchive, globalMiddleware...)) - s.mux.HandleFunc("GET /bookmark/{id}/archive/file/{file}", ToHTTPHandler(deps, handlers.HandleBookmarkArchiveFile, globalMiddleware...)) + s.mux.HandleFunc("GET /bookmark/{id}/archive/file/{path...}", ToHTTPHandler(deps, handlers.HandleBookmarkArchiveFile, globalMiddleware...)) s.mux.HandleFunc("GET /bookmark/{id}/thumb", ToHTTPHandler(deps, handlers.HandleBookmarkThumbnail, globalMiddleware...)) s.mux.HandleFunc("GET /bookmark/{id}/ebook", ToHTTPHandler(deps, handlers.HandleBookmarkEbook, globalMiddleware...)) @@ -97,10 +97,7 @@ func (s *HttpServer) Setup(cfg *config.Config, deps *dependencies.Dependencies) // API v1 routes // Auth s.mux.HandleFunc("POST /api/v1/auth/login", ToHTTPHandler(deps, - func(deps model.Dependencies, c model.WebContext) { - // TODO: Remove this once the legacy API is removed - api_v1.HandleLogin(deps, c, legacyHandler.HandleLogin) - }, + api_v1.HandleLogin, globalMiddleware..., )) s.mux.HandleFunc("POST /api/v1/auth/refresh", ToHTTPHandler(deps, diff --git a/internal/view/assets/js/component/login.js b/internal/view/assets/js/component/login.js index f8ddf7bf..f354759f 100644 --- a/internal/view/assets/js/component/login.js +++ b/internal/view/assets/js/component/login.js @@ -106,7 +106,7 @@ export default { } // Remove old cookie - document.cookie = `session-id=; Path=${ + document.cookie = `token=; Path=${ new URL(document.baseURI).pathname }; Expires=Thu, 01 Jan 1970 00:00:00 GMT;`; @@ -128,9 +128,6 @@ export default { }) .then((json) => { // Save session id - document.cookie = `session-id=${json.message.session}; Path=${ - new URL(document.baseURI).pathname - }; Expires=${new Date(json.message.expires * 1000).toUTCString()}`; document.cookie = `token=${json.message.token}; Path=${ new URL(document.baseURI).pathname }; Expires=${new Date(json.message.expires * 1000).toUTCString()}`; @@ -186,9 +183,6 @@ export default { } // Clear session data if we reach here - document.cookie = `session-id=; Path=${ - new URL(document.baseURI).pathname - }; Expires=Thu, 01 Jan 1970 00:00:00 GMT;`; document.cookie = `token=; Path=${ new URL(document.baseURI).pathname }; Expires=Thu, 01 Jan 1970 00:00:00 GMT;`; diff --git a/internal/view/assets/js/page/base.js b/internal/view/assets/js/page/base.js index 39fa1548..1a2345d7 100644 --- a/internal/view/assets/js/page/base.js +++ b/internal/view/assets/js/page/base.js @@ -135,7 +135,7 @@ export default { var loginUrl = new Url("login", document.baseURI); loginUrl.query.dst = window.location.href; - document.cookie = `session-id=; Path=${ + document.cookie = `token=; Path=${ new URL(document.baseURI).pathname }; Expires=Thu, 01 Jan 1970 00:00:00 GMT;`; location.href = loginUrl.toString(); diff --git a/internal/view/index.html b/internal/view/index.html index f0bb8ca0..23e10e77 100644 --- a/internal/view/index.html +++ b/internal/view/index.html @@ -95,14 +95,16 @@ mainClick: () => { this.dialog.loading = true; fetch(new URL("api/v1/auth/logout", document.baseURI), { - method: "post" + method: "post", + headers: { + "Authorization": `Bearer ${localStorage.getItem("shiori-token")}` + } }).then(response => { if (!response.ok) throw response; return response; }).then(() => { localStorage.removeItem("shiori-account"); localStorage.removeItem("shiori-token"); - document.cookie = `session-id=; Path=${new URL(document.baseURI).pathname}; Expires=Thu, 01 Jan 1970 00:00:00 GMT;`; document.cookie = `token=; Path=${new URL(document.baseURI).pathname}; Expires=Thu, 01 Jan 1970 00:00:00 GMT;`; this.isLoggedIn = false; this.loginRequired = true; @@ -189,7 +191,6 @@ // Clear invalid session data localStorage.removeItem("shiori-account"); localStorage.removeItem("shiori-token"); - document.cookie = `session-id=; Path=${new URL(document.baseURI).pathname}; Expires=Thu, 01 Jan 1970 00:00:00 GMT;`; document.cookie = `token=; Path=${new URL(document.baseURI).pathname}; Expires=Thu, 01 Jan 1970 00:00:00 GMT;`; return false; } @@ -202,6 +203,11 @@ if (isValid) { this.loadSetting(); this.loadAccount(); + + // Set the token cookie if empty + if (!document.cookie.includes("token")) { + document.cookie = `token=${localStorage.getItem("shiori-token")}; Path=${new URL(document.baseURI).pathname}; Expires=${new Date(Date.now() + 1000 * 60 * 60 * 24 * 30).toUTCString()};`; + } } else { this.loginRequired = true; } diff --git a/internal/webserver/handler.go b/internal/webserver/handler.go index 97164c0c..ef7adf2c 100644 --- a/internal/webserver/handler.go +++ b/internal/webserver/handler.go @@ -12,11 +12,11 @@ import ( // Handler is Handler for serving the web interface. type Handler struct { - DB model.DB - DataDir string - RootPath string - UserCache *cch.Cache - SessionCache *cch.Cache + DB model.DB + DataDir string + RootPath string + UserCache *cch.Cache + // SessionCache *cch.Cache ArchiveCache *cch.Cache Log bool @@ -24,86 +24,64 @@ type Handler struct { } func (h *Handler) PrepareSessionCache() { - h.SessionCache.OnEvicted(func(key string, val interface{}) { - account := val.(*model.AccountDTO) - arr, found := h.UserCache.Get(account.Username) - if !found { - return - } + // h.SessionCache.OnEvicted(func(key string, val interface{}) { + // account := val.(*model.AccountDTO) + // arr, found := h.UserCache.Get(account.Username) + // if !found { + // return + // } - sessionIDs := arr.([]string) - for i := 0; i < len(sessionIDs); i++ { - if sessionIDs[i] == key { - sessionIDs = append(sessionIDs[:i], sessionIDs[i+1:]...) - break - } - } + // sessionIDs := arr.([]string) + // for i := 0; i < len(sessionIDs); i++ { + // if sessionIDs[i] == key { + // sessionIDs = append(sessionIDs[:i], sessionIDs[i+1:]...) + // break + // } + // } - h.UserCache.Set(account.Username, sessionIDs, -1) - }) -} - -func (h *Handler) GetSessionID(r *http.Request) string { - // Try to get session ID from the header - sessionID := r.Header.Get("X-Session-Id") - - // If not, try it from the cookie - if sessionID == "" { - cookie, err := r.Cookie("session-id") - if err != nil { - return "" - } - - sessionID = cookie.Value - } - - return sessionID + // h.UserCache.Set(account.Username, sessionIDs, -1) + // }) } // validateSession checks whether user session is still valid or not func (h *Handler) validateSession(r *http.Request) error { authorization := r.Header.Get(model.AuthorizationHeader) + if authorization == "" { + // Get token from cookie + tokenCookie, err := r.Cookie("token") + if err != nil { + return fmt.Errorf("session is not exist") + } + + authorization = tokenCookie.Value + } + + var account *model.AccountDTO + if authorization != "" { + var err error + authParts := strings.SplitN(authorization, " ", 2) if len(authParts) != 2 && authParts[0] != model.AuthorizationTokenType { return fmt.Errorf("session has been expired") } - account, err := h.dependencies.Domains().Auth().CheckToken(r.Context(), authParts[1]) + account, err = h.dependencies.Domains().Auth().CheckToken(r.Context(), authParts[1]) if err != nil { return fmt.Errorf("session has been expired") } - - if r.Method != "" && r.Method != "GET" && account.Owner != nil && !*account.Owner { - return fmt.Errorf("account level is not sufficient") - } - - h.dependencies.Logger().WithFields(logrus.Fields{ - "username": account.Username, - "method": r.Method, - "path": r.URL.Path, - }).Info("allowing legacy api access using JWT token") - - return nil } - sessionID := h.GetSessionID(r) - if sessionID == "" { - return fmt.Errorf("session is not exist") + if r.Method != "" && r.Method != "GET" && account.Owner != nil && !*account.Owner { + return fmt.Errorf("account level is not sufficient") } - // Make sure session is not expired yet - val, found := h.SessionCache.Get(sessionID) - if !found { - return fmt.Errorf("session has been expired") - } - - // If this is not get request, make sure it's owner - if r.Method != "" && r.Method != "GET" { - if account := val.(*model.AccountDTO); account.Owner != nil && !*account.Owner { - return fmt.Errorf("account level is not sufficient") - } - } + h.dependencies.Logger().WithFields(logrus.Fields{ + "username": account.Username, + "method": r.Method, + "path": r.URL.Path, + }).Info("allowing legacy api access using JWT token") return nil + } diff --git a/internal/webserver/server.go b/internal/webserver/server.go index 8a828566..4cf59f5f 100644 --- a/internal/webserver/server.go +++ b/internal/webserver/server.go @@ -20,10 +20,10 @@ type Config struct { // GetLegacyHandler returns a legacy handler to use with the new webserver func GetLegacyHandler(cfg Config, dependencies model.Dependencies) *Handler { return &Handler{ - DB: cfg.DB, - DataDir: cfg.DataDir, - UserCache: cch.New(time.Hour, 10*time.Minute), - SessionCache: cch.New(time.Hour, 10*time.Minute), + DB: cfg.DB, + DataDir: cfg.DataDir, + UserCache: cch.New(time.Hour, 10*time.Minute), + // SessionCache: cch.New(time.Hour, 10*time.Minute), ArchiveCache: cch.New(time.Minute, 5*time.Minute), RootPath: cfg.RootPath, Log: cfg.Log,