From f698aaff4a8ce5ce77f342167d056543b299e3bd Mon Sep 17 00:00:00 2001 From: vibecoder11200 Date: Wed, 23 Sep 2026 23:40:41 +0700 Subject: [PATCH 1/5] fix(mediafire): always schedule session token renewal - Set up the renewal cron regardless of whether the initial getSessionToken call succeeds; previously the cron was only created when that call failed, so a valid login cookie resulted in no renewal and the session token expired after about 10 minutes - Run the cron callback with context.Background() so renewals no longer depend on the initialization request context - Fall back to minting a fresh token from the login cookie when the stored token can no longer be renewed - Return an error from Init when neither minting nor renewing yields a valid token instead of silently reporting a working storage --- drivers/mediafire/driver.go | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/drivers/mediafire/driver.go b/drivers/mediafire/driver.go index 49d431021d..432c81d90a 100644 --- a/drivers/mediafire/driver.go +++ b/drivers/mediafire/driver.go @@ -78,18 +78,24 @@ func (d *Mediafire) Init(ctx context.Context) error { // Validate and refresh session token if needed if _, err := d.getSessionToken(ctx); err != nil { - d.renewToken(ctx) - - // Avoids 10 mins token expiry (6- 9) - num := rand.Intn(4) + 6 + // Minting from the cookie failed; try renewing the stored token. + if renewErr := d.renewToken(ctx); renewErr != nil { + return fmt.Errorf("Init :: [MediaFire] failed to obtain a session token: %w", renewErr) + } + } - d.cron = cron.NewCron(time.Minute * time.Duration(num)) - d.cron.Do(func() { - // Crazy, but working way to refresh session token - d.renewToken(ctx) - }) + // MediaFire session tokens expire in ~10 minutes, so they must be + // renewed periodically no matter how the first token was obtained. + num := rand.Intn(4) + 6 - } + d.cron = cron.NewCron(time.Minute * time.Duration(num)) + d.cron.Do(func() { + // Renew while the token is still valid; if it already expired, + // mint a fresh one from the login cookie. + if err := d.renewToken(context.Background()); err != nil { + _, _ = d.getSessionToken(context.Background()) + } + }) return nil } From 840d203721540163bcbacf6233e2df626f555dae Mon Sep 17 00:00:00 2001 From: vibecoder11200 Date: Thu, 24 Sep 2026 03:32:16 +0700 Subject: [PATCH 2/5] fix(mediafire): guard session token access with RWMutex - Add mu sync.RWMutex with a sessionToken() accessor for request goroutines and a setSessionToken() writer used by both getSessionToken and renewToken; storage persistence now happens inside the write lock - Replace direct d.SessionToken reads across all request handlers with the locked accessor, fixing the data race introduced by running the renewal cron for every normally initialized storage --- drivers/mediafire/driver.go | 47 ++++++++++++++++++++++++++++--------- drivers/mediafire/util.go | 32 ++++++++----------------- 2 files changed, 46 insertions(+), 33 deletions(-) diff --git a/drivers/mediafire/driver.go b/drivers/mediafire/driver.go index 432c81d90a..d29c121040 100644 --- a/drivers/mediafire/driver.go +++ b/drivers/mediafire/driver.go @@ -20,11 +20,13 @@ import ( "math/rand" "net/http" "strconv" + "sync" "time" "github.com/OpenListTeam/OpenList/v4/drivers/base" "github.com/OpenListTeam/OpenList/v4/internal/driver" "github.com/OpenListTeam/OpenList/v4/internal/model" + "github.com/OpenListTeam/OpenList/v4/internal/op" "github.com/OpenListTeam/OpenList/v4/internal/stream" "github.com/OpenListTeam/OpenList/v4/pkg/cron" "github.com/OpenListTeam/OpenList/v4/pkg/utils" @@ -37,6 +39,10 @@ type Mediafire struct { cron *cron.Cron + // mu guards SessionToken and Cookie: request goroutines read the token + // while the renewal cron may refresh it in the background. + mu sync.RWMutex + actionToken string limiter *rate.Limiter @@ -50,6 +56,25 @@ type Mediafire struct { userAgent string } +// sessionToken returns the current session token for request building. +func (d *Mediafire) sessionToken() string { + d.mu.RLock() + defer d.mu.RUnlock() + return d.SessionToken +} + +// setSessionToken stores a refreshed token (and login cookie when provided) +// and persists them, safe to call from the renewal cron goroutine. +func (d *Mediafire) setSessionToken(token, cookie string) { + d.mu.Lock() + defer d.mu.Unlock() + if cookie != "" { + d.Cookie = cookie + } + d.SessionToken = token + op.MustSaveDriverStorage(d) +} + func (d *Mediafire) Config() driver.Config { return config } @@ -65,7 +90,7 @@ func (d *Mediafire) Init(ctx context.Context) error { } // If SessionToken is empty, try to get it from cookie - if d.SessionToken == "" { + if d.sessionToken() == "" { if _, err := d.getSessionToken(ctx); err != nil { return fmt.Errorf("Init :: [MediaFire] {critical} failed to get session token from cookie: %w", err) } @@ -156,7 +181,7 @@ func (d *Mediafire) Link(ctx context.Context, file model.Obj, args model.LinkArg // MakeDir creates a new folder in the specified parent directory func (d *Mediafire) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) (model.Obj, error) { data := map[string]string{ - "session_token": d.SessionToken, + "session_token": d.sessionToken(), "response_format": "json", "parent_key": parentDir.GetID(), "foldername": dirName, @@ -193,7 +218,7 @@ func (d *Mediafire) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.O endpoint = "/folder/move.php" data = map[string]string{ - "session_token": d.SessionToken, + "session_token": d.sessionToken(), "response_format": "json", "folder_key_src": srcObj.GetID(), "folder_key_dst": dstDir.GetID(), @@ -202,7 +227,7 @@ func (d *Mediafire) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.O endpoint = "/file/move.php" data = map[string]string{ - "session_token": d.SessionToken, + "session_token": d.sessionToken(), "response_format": "json", "quick_key": srcObj.GetID(), "folder_key": dstDir.GetID(), @@ -231,7 +256,7 @@ func (d *Mediafire) Rename(ctx context.Context, srcObj model.Obj, newName string endpoint = "/folder/update.php" data = map[string]string{ - "session_token": d.SessionToken, + "session_token": d.sessionToken(), "response_format": "json", "folder_key": srcObj.GetID(), "foldername": newName, @@ -240,7 +265,7 @@ func (d *Mediafire) Rename(ctx context.Context, srcObj model.Obj, newName string endpoint = "/file/update.php" data = map[string]string{ - "session_token": d.SessionToken, + "session_token": d.sessionToken(), "response_format": "json", "quick_key": srcObj.GetID(), "filename": newName, @@ -276,7 +301,7 @@ func (d *Mediafire) Copy(ctx context.Context, srcObj, dstDir model.Obj) (model.O endpoint = "/folder/copy.php" data = map[string]string{ - "session_token": d.SessionToken, + "session_token": d.sessionToken(), "response_format": "json", "folder_key_src": srcObj.GetID(), "folder_key_dst": dstDir.GetID(), @@ -285,7 +310,7 @@ func (d *Mediafire) Copy(ctx context.Context, srcObj, dstDir model.Obj) (model.O endpoint = "/file/copy.php" data = map[string]string{ - "session_token": d.SessionToken, + "session_token": d.sessionToken(), "response_format": "json", "quick_key": srcObj.GetID(), "folder_key": dstDir.GetID(), @@ -332,7 +357,7 @@ func (d *Mediafire) Remove(ctx context.Context, obj model.Obj) error { endpoint = "/folder/delete.php" data = map[string]string{ - "session_token": d.SessionToken, + "session_token": d.sessionToken(), "response_format": "json", "folder_key": obj.GetID(), } @@ -340,7 +365,7 @@ func (d *Mediafire) Remove(ctx context.Context, obj model.Obj) error { endpoint = "/file/delete.php" data = map[string]string{ - "session_token": d.SessionToken, + "session_token": d.sessionToken(), "response_format": "json", "quick_key": obj.GetID(), } @@ -414,7 +439,7 @@ func (d *Mediafire) Put(ctx context.Context, dstDir model.Obj, file model.FileSt func (d *Mediafire) GetDetails(ctx context.Context) (*model.StorageDetails, error) { data := map[string]string{ - "session_token": d.SessionToken, + "session_token": d.sessionToken(), "response_format": "json", } var resp MediafireUserInfoResponse diff --git a/drivers/mediafire/util.go b/drivers/mediafire/util.go index 49711fc5e7..d323f8fc00 100644 --- a/drivers/mediafire/util.go +++ b/drivers/mediafire/util.go @@ -29,7 +29,6 @@ import ( "github.com/OpenListTeam/OpenList/v4/drivers/base" "github.com/OpenListTeam/OpenList/v4/internal/driver" "github.com/OpenListTeam/OpenList/v4/internal/model" - "github.com/OpenListTeam/OpenList/v4/internal/op" "github.com/OpenListTeam/OpenList/v4/internal/stream" "github.com/OpenListTeam/OpenList/v4/pkg/errgroup" "github.com/OpenListTeam/OpenList/v4/pkg/utils" @@ -110,6 +109,7 @@ func (d *Mediafire) getSessionToken(ctx context.Context) (string, error) { } `json:"response"` } + var cookie string if resp.StatusCode == 200 { if err := json.Unmarshal(body, &tokenResp); err != nil { return "", err @@ -125,34 +125,26 @@ func (d *Mediafire) getSessionToken(ctx context.Context) (string, error) { } if len(cookieMap) > 0 { - var cookies []string for name, value := range cookieMap { cookies = append(cookies, fmt.Sprintf("%s=%s", name, value)) } - d.Cookie = strings.Join(cookies, "; ") - op.MustSaveDriverStorage(d) - - // fmt.Printf("getSessionToken :: Captured cookies: %s\n", d.Cookie) + cookie = strings.Join(cookies, "; ") } } else { return "", fmt.Errorf("getSessionToken :: failed to get session token, status code: %d", resp.StatusCode) } - d.SessionToken = tokenResp.Response.SessionToken - - // fmt.Printf("Init :: Obtain Session Token %v", d.SessionToken) + d.setSessionToken(tokenResp.Response.SessionToken, cookie) - op.MustSaveDriverStorage(d) - - return d.SessionToken, nil + return d.sessionToken(), nil } // renewToken refreshes the current session token when expired func (d *Mediafire) renewToken(ctx context.Context) error { query := map[string]string{ - "session_token": d.SessionToken, + "session_token": d.sessionToken(), "response_format": "json", } @@ -169,11 +161,7 @@ func (d *Mediafire) renewToken(ctx context.Context) error { return fmt.Errorf("MediaFire token renewal failed: %s", resp.Response.Result) } - d.SessionToken = resp.Response.SessionToken - - // fmt.Printf("Init :: Renew Session Token: %s", resp.Response.Result) - - op.MustSaveDriverStorage(d) + d.setSessionToken(resp.Response.SessionToken, "") return nil } @@ -247,7 +235,7 @@ func (d *Mediafire) getFolderContent(ctx context.Context, folderKey string, chun func (d *Mediafire) getFolderContentByType(ctx context.Context, folderKey, contentType string, chunkNumber int) (*MediafireResponse, error) { data := map[string]string{ - "session_token": d.SessionToken, + "session_token": d.sessionToken(), "response_format": "json", "folder_key": folderKey, "content_type": contentType, @@ -364,7 +352,7 @@ func (d *Mediafire) postForm(ctx context.Context, endpoint string, data map[stri func (d *Mediafire) getDirectDownloadLink(ctx context.Context, fileID string) (string, error) { data := map[string]string{ - "session_token": d.SessionToken, + "session_token": d.sessionToken(), "quick_key": fileID, "link_type": "direct_download", "response_format": "json", @@ -629,7 +617,7 @@ func (d *Mediafire) getActionToken(ctx context.Context) (string, error) { "type": "upload", "lifespan": "1440", "response_format": "json", - "session_token": d.SessionToken, + "session_token": d.sessionToken(), } var resp MediafireActionTokenResponse @@ -710,7 +698,7 @@ func (d *Mediafire) getExistingFileInfo(ctx context.Context, fileHash, filename, func (d *Mediafire) getFileByHash(ctx context.Context, hash string) (*model.ObjThumb, error) { query := map[string]string{ - "session_token": d.SessionToken, + "session_token": d.sessionToken(), "response_format": "json", "hash": hash, } From 0ca3759d0b7767e152388e913b8535ad4313651a Mon Sep 17 00:00:00 2001 From: vibecoder11200 Date: Thu, 24 Sep 2026 03:45:30 +0700 Subject: [PATCH 3/5] fix(mediafire): harden concurrent access beyond the session token - Add a locked cookie() accessor: setCommonHeaders reads the login cookie on every API request while the renewal cron may rewrite it - Always schedule the renewal cron in Init; a transient mint/renew failure no longer leaves the storage without self-healing, and both failures are now logged instead of silently discarded - Cache the upload action token under the same mutex: Drop clears it safely against in-flight uploads, and uploads stop minting a fresh action token for every chunk - Return the locally minted token from getSessionToken and surface the gzip decompression error instead of swallowing it --- drivers/mediafire/driver.go | 41 +++++++++++++++++++++++-------------- drivers/mediafire/util.go | 19 +++++++++++------ 2 files changed, 39 insertions(+), 21 deletions(-) diff --git a/drivers/mediafire/driver.go b/drivers/mediafire/driver.go index d29c121040..087a12b0f9 100644 --- a/drivers/mediafire/driver.go +++ b/drivers/mediafire/driver.go @@ -30,6 +30,7 @@ import ( "github.com/OpenListTeam/OpenList/v4/internal/stream" "github.com/OpenListTeam/OpenList/v4/pkg/cron" "github.com/OpenListTeam/OpenList/v4/pkg/utils" + log "github.com/sirupsen/logrus" "golang.org/x/time/rate" ) @@ -63,8 +64,17 @@ func (d *Mediafire) sessionToken() string { return d.SessionToken } +// cookie returns the current login cookie for request building. +func (d *Mediafire) cookie() string { + d.mu.RLock() + defer d.mu.RUnlock() + return d.Cookie +} + // setSessionToken stores a refreshed token (and login cookie when provided) -// and persists them, safe to call from the renewal cron goroutine. +// and persists them, safe to call from the renewal cron goroutine. Note that +// framework-driven re-Init (admin storage update) rewrites the addition +// outside this lock; the framework serializes those via Drop. func (d *Mediafire) setSessionToken(token, cookie string) { d.mu.Lock() defer d.mu.Unlock() @@ -85,27 +95,22 @@ func (d *Mediafire) GetAddition() driver.Additional { // Init initializes the MediaFire driver with session token and cookie validation func (d *Mediafire) Init(ctx context.Context) error { - if d.Cookie == "" { + if d.cookie() == "" { return fmt.Errorf("Init :: [MediaFire] {critical} missing Cookie") } - // If SessionToken is empty, try to get it from cookie - if d.sessionToken() == "" { - if _, err := d.getSessionToken(ctx); err != nil { - return fmt.Errorf("Init :: [MediaFire] {critical} failed to get session token from cookie: %w", err) - } - } - // Setup rate limiter if rate limit is configured if d.LimitRate > 0 { d.limiter = rate.NewLimiter(rate.Limit(d.LimitRate), 1) } - // Validate and refresh session token if needed + // Obtain a session token: mint from the login cookie first, fall back to + // renewing the stored token. A transient failure here must not skip the + // scheduling below — the renewal cron self-heals on its next tick. if _, err := d.getSessionToken(ctx); err != nil { - // Minting from the cookie failed; try renewing the stored token. if renewErr := d.renewToken(ctx); renewErr != nil { - return fmt.Errorf("Init :: [MediaFire] failed to obtain a session token: %w", renewErr) + log.Warnf("mediafire[%s]: could not obtain a session token (mint: %v, renew: %v); will retry on the renewal cron", + d.MountPath, err, renewErr) } } @@ -118,7 +123,9 @@ func (d *Mediafire) Init(ctx context.Context) error { // Renew while the token is still valid; if it already expired, // mint a fresh one from the login cookie. if err := d.renewToken(context.Background()); err != nil { - _, _ = d.getSessionToken(context.Background()) + if _, mintErr := d.getSessionToken(context.Background()); mintErr != nil { + log.Warnf("mediafire[%s]: session token renewal failed: %v", d.MountPath, mintErr) + } } }) @@ -127,12 +134,16 @@ func (d *Mediafire) Init(ctx context.Context) error { // Drop cleans up driver resources func (d *Mediafire) Drop(ctx context.Context) error { - // Clear cached resources - d.actionToken = "" + // Stop the cron first so no in-flight renewal can repopulate the + // action token after it is cleared, then clear under the lock so a + // concurrent upload worker cannot read a stale value. if d.cron != nil { d.cron.Stop() d.cron = nil } + d.mu.Lock() + d.actionToken = "" + d.mu.Unlock() return nil } diff --git a/drivers/mediafire/util.go b/drivers/mediafire/util.go index d323f8fc00..459d34d437 100644 --- a/drivers/mediafire/util.go +++ b/drivers/mediafire/util.go @@ -63,7 +63,7 @@ func (d *Mediafire) getSessionToken(ctx context.Context) (string, error) { req.Header.Set("Accept-Encoding", "gzip") req.Header.Set("Accept-Language", "en-US,en;q=0.9") req.Header.Set("Content-Length", "0") - req.Header.Set("Cookie", d.Cookie) + req.Header.Set("Cookie", d.cookie()) req.Header.Set("DNT", "1") req.Header.Set("Origin", d.hostBase) req.Header.Set("Priority", "u=1, i") @@ -91,7 +91,7 @@ func (d *Mediafire) getSessionToken(ctx context.Context) (string, error) { return "", fmt.Errorf("failed to create gzip reader: %w", err) } defer gzipReader.Close() - body, _ = io.ReadAll(gzipReader) + body, err = io.ReadAll(gzipReader) } else { body, err = io.ReadAll(resp.Body) } @@ -138,7 +138,7 @@ func (d *Mediafire) getSessionToken(ctx context.Context) (string, error) { d.setSessionToken(tokenResp.Response.SessionToken, cookie) - return d.sessionToken(), nil + return tokenResp.Response.SessionToken, nil } // renewToken refreshes the current session token when expired @@ -287,7 +287,7 @@ func (d *Mediafire) fileToObj(f File) *model.ObjThumb { func (d *Mediafire) setCommonHeaders(req *resty.Request) { req.SetHeaders(map[string]string{ - "Cookie": d.Cookie, + "Cookie": d.cookie(), "User-Agent": d.userAgent, "Origin": d.appBase, "Referer": d.appBase + "/", @@ -609,8 +609,11 @@ func (d *Mediafire) uploadUnits(ctx context.Context, file model.FileStreamer, ch }*/ func (d *Mediafire) getActionToken(ctx context.Context) (string, error) { - if d.actionToken != "" { - return d.actionToken, nil + d.mu.RLock() + cached := d.actionToken + d.mu.RUnlock() + if cached != "" { + return cached, nil } data := map[string]string{ @@ -630,6 +633,10 @@ func (d *Mediafire) getActionToken(ctx context.Context) (string, error) { return "", fmt.Errorf("MediaFire action token failed: %s", resp.Response.Result) } + d.mu.Lock() + d.actionToken = resp.Response.ActionToken + d.mu.Unlock() + return resp.Response.ActionToken, nil } From 8d9f1e57a23f843e11a5700e2faf5769d0667d40 Mon Sep 17 00:00:00 2001 From: vibecoder11200 Date: Thu, 24 Sep 2026 17:56:11 +0700 Subject: [PATCH 4/5] fix(mediafire): drain in-flight renewals on Drop and fail Init without a token - Return an error from Init when neither minting nor renewing yields a session token, so dead credentials surface at mount time instead of a storage that reports work but fails every token-dependent operation; the renewal cron stays scheduled so transient failures self-heal - Run the renewal callback on a driver-owned cancellable context instead of context.Background(), and add a sync.WaitGroup so Drop cancels, stops the cron, and waits for any in-flight renewal before returning; no renewal can overwrite the token or persist the storage after Drop or during a re-Init - Tear down any leftover renewal lifecycle at the very top of Init, covering paths that reach a second Init without a Drop in between (an update that emptied the Cookie, panic recovery in initStorage) - Rewrite the setSessionToken comment to state the actual guarantee, and document on stopRenewal that it relies on Cron.Stop blocking until the dispatch goroutine has exited - Log both the renew and mint errors when a cron renewal fails --- drivers/mediafire/driver.go | 83 ++++++++++++++++++++++++++++++------- 1 file changed, 68 insertions(+), 15 deletions(-) diff --git a/drivers/mediafire/driver.go b/drivers/mediafire/driver.go index 087a12b0f9..e3837fe88c 100644 --- a/drivers/mediafire/driver.go +++ b/drivers/mediafire/driver.go @@ -40,6 +40,13 @@ type Mediafire struct { cron *cron.Cron + // Renewal lifecycle: renewCancel aborts in-flight renewals and renewWG + // lets Drop drain them, so no renewal outlives Drop and races with a + // re-Init on this same instance (storage updates reuse it). + renewCtx context.Context + renewCancel context.CancelFunc + renewWG sync.WaitGroup + // mu guards SessionToken and Cookie: request goroutines read the token // while the renewal cron may refresh it in the background. mu sync.RWMutex @@ -72,9 +79,10 @@ func (d *Mediafire) cookie() string { } // setSessionToken stores a refreshed token (and login cookie when provided) -// and persists them, safe to call from the renewal cron goroutine. Note that -// framework-driven re-Init (admin storage update) rewrites the addition -// outside this lock; the framework serializes those via Drop. +// and persists them, safe to call from the renewal cron goroutine. +// Framework-driven re-Init (admin storage update) rewrites the addition +// outside this lock; that is safe because Drop cancels the renewal context +// and drains any in-flight renewal before Init runs again. func (d *Mediafire) setSessionToken(token, cookie string) { d.mu.Lock() defer d.mu.Unlock() @@ -95,6 +103,12 @@ func (d *Mediafire) GetAddition() driver.Additional { // Init initializes the MediaFire driver with session token and cookie validation func (d *Mediafire) Init(ctx context.Context) error { + // Storage updates re-Initialize this same instance (Drop then Init). + // Tear any leftover lifecycle down before anything else can return, in + // case Init is entered twice without a Drop in between (e.g. after a + // panic inside a previous Init, or an update that emptied the Cookie). + d.stopRenewal() + if d.cookie() == "" { return fmt.Errorf("Init :: [MediaFire] {critical} missing Cookie") } @@ -105,12 +119,13 @@ func (d *Mediafire) Init(ctx context.Context) error { } // Obtain a session token: mint from the login cookie first, fall back to - // renewing the stored token. A transient failure here must not skip the - // scheduling below — the renewal cron self-heals on its next tick. + // renewing the stored token. The failure is remembered but not returned + // yet, so the cron below is still scheduled and a transient failure + // (e.g. DNS not up at container boot) self-heals on a later tick. + var tokenErr error if _, err := d.getSessionToken(ctx); err != nil { if renewErr := d.renewToken(ctx); renewErr != nil { - log.Warnf("mediafire[%s]: could not obtain a session token (mint: %v, renew: %v); will retry on the renewal cron", - d.MountPath, err, renewErr) + tokenErr = fmt.Errorf("failed to get session token (mint: %v, renew: %v)", err, renewErr) } } @@ -118,29 +133,67 @@ func (d *Mediafire) Init(ctx context.Context) error { // renewed periodically no matter how the first token was obtained. num := rand.Intn(4) + 6 + d.renewCtx, d.renewCancel = context.WithCancel(context.Background()) + renewCtx := d.renewCtx d.cron = cron.NewCron(time.Minute * time.Duration(num)) d.cron.Do(func() { + // Join the WaitGroup before checking cancellation: either Drop + // observes this callback in renewWG.Wait, or the check turns it + // into a no-op. + d.renewWG.Add(1) + defer d.renewWG.Done() + if utils.IsCanceled(renewCtx) { + return + } // Renew while the token is still valid; if it already expired, // mint a fresh one from the login cookie. - if err := d.renewToken(context.Background()); err != nil { - if _, mintErr := d.getSessionToken(context.Background()); mintErr != nil { - log.Warnf("mediafire[%s]: session token renewal failed: %v", d.MountPath, mintErr) + if err := d.renewToken(renewCtx); err != nil { + if _, mintErr := d.getSessionToken(renewCtx); mintErr != nil && !utils.IsCanceled(renewCtx) { + log.Warnf("mediafire[%s]: session token renewal failed (renew: %v, mint: %v)", d.MountPath, err, mintErr) } } }) + // Dead credentials must surface at mount time instead of leaving a + // storage that reports work but fails every token-dependent operation. + if tokenErr != nil { + return fmt.Errorf("Init :: [MediaFire] {critical} %w", tokenErr) + } return nil } -// Drop cleans up driver resources -func (d *Mediafire) Drop(ctx context.Context) error { - // Stop the cron first so no in-flight renewal can repopulate the - // action token after it is cleared, then clear under the lock so a - // concurrent upload worker cannot read a stale value. +// stopRenewal stops renewal scheduling and drains any in-flight renewal: +// the canceled context aborts its HTTP calls, cron.Stop stops future ticks +// (it blocks until the dispatch goroutine has exited, so no callback can be +// dispatched after it returns), and renewWG.Wait blocks until the callback +// has finished, so once this returns no renewal can still write the token, +// cookie, or persisted storage. +func (d *Mediafire) stopRenewal() { + if d.renewCancel != nil { + // Cancel before Stop: an in-flight callback then aborts its HTTP + // calls instead of running to completion first. + d.renewCancel() + } if d.cron != nil { d.cron.Stop() d.cron = nil } + if d.renewCancel != nil { + d.renewWG.Wait() + d.renewCancel = nil + d.renewCtx = nil + } +} + +// Drop cleans up driver resources +func (d *Mediafire) Drop(ctx context.Context) error { + // Drain the renewal lifecycle before touching shared state: the + // framework re-Initializes this same instance on storage updates, and a + // renewal surviving past this point could overwrite the token or + // persist the storage mid-re-Init. + d.stopRenewal() + // Clear the action token under the lock so a concurrent upload worker + // cannot keep reading a stale value. d.mu.Lock() d.actionToken = "" d.mu.Unlock() From 8b5199544a6eb42a1b903a08455936c043577fee Mon Sep 17 00:00:00 2001 From: vibecoder11200 Date: Thu, 24 Sep 2026 20:30:22 +0700 Subject: [PATCH 5/5] fix(mediafire): merge mint response cookies instead of replacing - The get_session_token response only re-issues Cloudflare cookies (__cf_bm); the auth cookies (ukey, skey, session, user, cf_clearance) come from the login page and are never re-sent, so replacing the stored cookie with the response cookies dropped them on every successful mint - A storage kept working only while its session token stayed renewable; after a restart, or any gap that let the token expire, every mint returned 401 until the login cookie was re-entered manually - Merge fresh response cookies into the stored header so __cf_bm refreshes in place and the auth cookies survive every mint --- drivers/mediafire/util.go | 39 +++++++++++++++++++++++++++------------ 1 file changed, 27 insertions(+), 12 deletions(-) diff --git a/drivers/mediafire/util.go b/drivers/mediafire/util.go index 459d34d437..45123948be 100644 --- a/drivers/mediafire/util.go +++ b/drivers/mediafire/util.go @@ -44,6 +44,28 @@ func checkAPIResult(result string) error { return nil } +// mergeCookies overlays fresh response cookies onto the stored cookie +// header, preserving cookies the endpoint did not re-issue. +func mergeCookies(currentHeader string, fresh []*http.Cookie) string { + jar := make(map[string]string) + for _, part := range strings.Split(currentHeader, "; ") { + if part == "" { + continue + } + if i := strings.Index(part, "="); i > 0 { + jar[part[:i]] = part[i+1:] + } + } + for _, c := range fresh { + jar[c.Name] = c.Value + } + pairs := make([]string, 0, len(jar)) + for name, value := range jar { + pairs = append(pairs, name+"="+value) + } + return strings.Join(pairs, "; ") +} + // getSessionToken retrieves and validates session token from MediaFire func (d *Mediafire) getSessionToken(ctx context.Context) (string, error) { if d.limiter != nil { @@ -119,18 +141,11 @@ func (d *Mediafire) getSessionToken(ctx context.Context) (string, error) { return "", fmt.Errorf("empty session token received") } - cookieMap := make(map[string]string) - for _, cookie := range resp.Cookies() { - cookieMap[cookie.Name] = cookie.Value - } - - if len(cookieMap) > 0 { - var cookies []string - for name, value := range cookieMap { - cookies = append(cookies, fmt.Sprintf("%s=%s", name, value)) - } - cookie = strings.Join(cookies, "; ") - } + // The mint endpoint only re-issues Cloudflare cookies (__cf_bm); + // the auth cookies (ukey, skey, session, user, ...) come from the + // login page and are never re-sent. Merge instead of replacing, or + // every successful mint would drop them and break all future mints. + cookie = mergeCookies(d.cookie(), resp.Cookies()) } else { return "", fmt.Errorf("getSessionToken :: failed to get session token, status code: %d", resp.StatusCode)