Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 33 additions & 5 deletions drivers/115_open/driver.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,17 +40,45 @@ func (d *Open115) GetAddition() driver.Additional {
return &d.Addition
}

// onTokenRefreshed persists the rotated token pair. 115 Open refresh tokens are
// single use: once a new one is issued the previous one is rejected, so a lost
// write would leave the storage broken after a restart.
//
// The SDK assigns the response to its own fields before invoking this callback,
// so an empty refresh token in the response would also clear the value used by
// the next refresh request. Restore both states from the addition, which keeps
// the last usable token when the field is missing.
func (d *Open115) onTokenRefreshed(accessToken, refreshToken string) {
if accessToken != "" {
d.Addition.AccessToken = accessToken
}
if refreshToken != "" {
d.Addition.RefreshToken = refreshToken
}
if d.client != nil {
d.client.SetAccessToken(d.Addition.AccessToken)
d.client.SetRefreshToken(d.Addition.RefreshToken)
}
op.MustSaveDriverStorage(d)
}

func (d *Open115) Init(ctx context.Context) error {
if d.Addition.AccessToken == "" && d.Addition.RefreshToken == "" {
return errors.New("access token or refresh token is required")
}
d.client = sdk.New(sdk.WithRefreshToken(d.Addition.RefreshToken),
sdk.WithAccessToken(d.Addition.AccessToken),
sdk.WithOnRefreshToken(func(s1, s2 string) {
d.Addition.AccessToken = s1
d.Addition.RefreshToken = s2
op.MustSaveDriverStorage(d)
}))
sdk.WithOnRefreshToken(d.onTokenRefreshed))
if flags.Debug || flags.Dev {
d.client.SetDebug(true)
}
if d.Addition.AccessToken == "" {
// Without an access token the SDK would send an unauthenticated request
// first and only then refresh, surfacing a misleading "40100000 参数错误".
if _, err := d.client.RefreshToken(ctx); err != nil {
return err
}
}
_, err := d.client.UserInfo(ctx)
if err != nil {
return err
Expand Down
129 changes: 129 additions & 0 deletions drivers/115_open/driver_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
package _115_open

import (
"encoding/json"
"fmt"
"reflect"
"sync"
"testing"

sdk "github.com/OpenListTeam/115-sdk-go"
"github.com/OpenListTeam/OpenList/v4/internal/conf"
"github.com/OpenListTeam/OpenList/v4/internal/db"
"github.com/OpenListTeam/OpenList/v4/internal/model"
"github.com/glebarez/sqlite"
"gorm.io/gorm"
)

var (
setupDBOnce sync.Once
setupDBErr error
rowSeq int64
)

// setupTestDB mirrors internal/op/storage_test.go: an in-memory SQLite database
// behind internal/db, so op.MustSaveDriverStorage really persists and the tests
// can assert on the stored row.
func setupTestDB(t *testing.T) {
t.Helper()
setupDBOnce.Do(func() {
var gormDB *gorm.DB
gormDB, setupDBErr = gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{})
if setupDBErr != nil {
return
}
conf.Conf = conf.DefaultConfig("testdata")
db.Init(gormDB)
})
if setupDBErr != nil {
t.Fatalf("failed to set up test database: %v", setupDBErr)
}
}

func newTestDriver(t *testing.T, accessToken, refreshToken string) *Open115 {
t.Helper()
setupTestDB(t)
rowSeq++
addition, err := json.Marshal(Addition{AccessToken: accessToken, RefreshToken: refreshToken})
if err != nil {
t.Fatalf("failed to marshal addition: %v", err)
}
row := &model.Storage{
Driver: "115 Open",
MountPath: fmt.Sprintf("/115open-test-%d", rowSeq),
Addition: string(addition),
}
if err := db.CreateStorage(row); err != nil {
t.Fatalf("failed to create storage row: %v", err)
}
d := &Open115{Storage: *row}
d.Addition.AccessToken = accessToken
d.Addition.RefreshToken = refreshToken
d.client = sdk.New(sdk.WithAccessToken(accessToken), sdk.WithRefreshToken(refreshToken))
d.client.SetOnRefreshToken(d.onTokenRefreshed)
return d
}

func persistedRefreshToken(t *testing.T, id uint) string {
t.Helper()
st, err := db.GetStorageById(id)
if err != nil {
t.Fatalf("failed to read storage back: %v", err)
}
var a Addition
if err := json.Unmarshal([]byte(st.Addition), &a); err != nil {
t.Fatalf("failed to decode persisted addition %q: %v", st.Addition, err)
}
return a.RefreshToken
}

func clientField(t *testing.T, c *sdk.Client, name string) string {
t.Helper()
v := reflect.ValueOf(c).Elem().FieldByName(name)
if !v.IsValid() {
t.Fatalf("field %s not found on sdk.Client", name)
}
return v.String()
}

// The SDK assigns the response to its own fields before invoking the callback,
// so an empty refresh token in the response must not survive in either the
// persisted addition or the running client.
func TestOnTokenRefreshedKeepsLastRefreshTokenWhenResponseOmitsIt(t *testing.T) {
d := newTestDriver(t, "old-access", "old-refresh")

// state the SDK leaves behind after a response without refresh_token
d.client.SetAccessToken("")
d.client.SetRefreshToken("")

d.onTokenRefreshed("new-access", "")

if got := persistedRefreshToken(t, d.ID); got != "old-refresh" {
t.Errorf("persisted refresh token = %q, want %q", got, "old-refresh")
}
if got := d.Addition.RefreshToken; got != "old-refresh" {
t.Errorf("addition refresh token = %q, want %q", got, "old-refresh")
}
if got := clientField(t, d.client, "refreshToken"); got != "old-refresh" {
t.Errorf("sdk refresh token = %q, want %q", got, "old-refresh")
}
if got := clientField(t, d.client, "accessToken"); got != "new-access" {
t.Errorf("sdk access token = %q, want %q", got, "new-access")
}
}

func TestOnTokenRefreshedStoresRotatedPair(t *testing.T) {
d := newTestDriver(t, "old-access", "old-refresh")

d.onTokenRefreshed("new-access", "new-refresh")

if got := persistedRefreshToken(t, d.ID); got != "new-refresh" {
t.Errorf("persisted refresh token = %q, want %q", got, "new-refresh")
}
if got := clientField(t, d.client, "refreshToken"); got != "new-refresh" {
t.Errorf("sdk refresh token = %q, want %q", got, "new-refresh")
}
if got := clientField(t, d.client, "accessToken"); got != "new-access" {
t.Errorf("sdk access token = %q, want %q", got, "new-access")
}
}
20 changes: 18 additions & 2 deletions internal/bootstrap/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,7 @@ func InitDB() {
if !(strings.HasSuffix(database.DBFile, ".db") && len(database.DBFile) > 3) {
log.Fatalf("db name error.")
}
dB, err = gorm.Open(openSQLite(fmt.Sprintf("%s?_journal=WAL&_vacuum=incremental",
database.DBFile)), gormConfig)
dB, err = gorm.Open(openSQLite(database.DBFile), gormConfig)
}
case "mysql":
{
Expand Down Expand Up @@ -86,3 +85,20 @@ func InitDB() {
}
db.Init(dB)
}

// appendDSNParams appends SQLite DSN query parameters while keeping the ones
// already present. The accepted pragma syntax differs between the pure-Go and
// the cgo builds, so each build supplies its own parameter list.
func appendDSNParams(dsn, params string) string {
if params == "" {
return dsn
}
switch {
case strings.HasSuffix(dsn, "?"), strings.HasSuffix(dsn, "&"):
return dsn + params
case strings.Contains(dsn, "?"):
return dsn + "&" + params
default:
return dsn + "?" + params
}
}
8 changes: 7 additions & 1 deletion internal/bootstrap/sqlite_driver_glebarez.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,11 @@ import (
)

func openSQLite(dsn string) gorm.Dialector {
return sqlite.Open(dsn)
// glebarez/go-sqlite only understands "_pragma=" parameters; the mattn style
// "_journal=", "_busy_timeout=" ones are silently ignored, which left the
// database in rollback-journal mode with the driver default 5s busy timeout.
// Readers and the single writer then block each other, so concurrent saves
// (e.g. a rotated 115 Open refresh token) fail with SQLITE_BUSY.
return sqlite.Open(appendDSNParams(dsn,
"_pragma=journal_mode(WAL)&_pragma=busy_timeout(10000)&_pragma=synchronous(NORMAL)"))
}
3 changes: 2 additions & 1 deletion internal/bootstrap/sqlite_driver_gorm.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@ import (
)

func openSQLite(dsn string) gorm.Dialector {
return sqlite.Open(dsn)
return sqlite.Open(appendDSNParams(dsn,
"_journal=WAL&_vacuum=incremental&_busy_timeout=10000&_synchronous=NORMAL"))
}
36 changes: 35 additions & 1 deletion internal/op/storage.go
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,40 @@ func MustSaveDriverStorage(driver driver.Driver) {
}
}

// Retrying is required because several drivers persist rotating credentials
// here (115 Open, PikPak, OneDrive...). The server invalidates the previous
// token as soon as a new one is issued, so a lost write leaves the storage with
// a permanently unusable token once the process restarts.
const (
storageSaveAttempts = 5
storageSaveBaseDelay = 200 * time.Millisecond
)

func isTransientDBError(err error) bool {
msg := strings.ToLower(err.Error())
return strings.Contains(msg, "database is locked") ||
strings.Contains(msg, "sqlite_busy") ||
strings.Contains(msg, "database table is locked")
}

func updateStorageWithRetry(storage *model.Storage) error {
var err error
delay := storageSaveBaseDelay
for attempt := 0; attempt < storageSaveAttempts; attempt++ {
if err = db.UpdateStorage(storage); err == nil {
return nil
}
if !isTransientDBError(err) {
return err
}
if attempt < storageSaveAttempts-1 {
time.Sleep(delay)
delay *= 2
}
}
return err
}

func saveDriverStorage(driver driver.Driver) error {
storage := driver.GetStorage()
addition := driver.GetAddition()
Expand All @@ -304,7 +338,7 @@ func saveDriverStorage(driver driver.Driver) error {
return errors.Wrap(err, "error while marshal addition")
}
storage.Addition = str
err = db.UpdateStorage(storage)
err = updateStorageWithRetry(storage)
if err != nil {
return errors.WithMessage(err, "failed update storage in database")
}
Expand Down
Loading