diff --git a/.gitignore b/.gitignore index 971221d..e57e9e3 100644 --- a/.gitignore +++ b/.gitignore @@ -25,5 +25,8 @@ go.work.sum # env file .env -# Google config -config.json \ No newline at end of file +# Database +*.db + +# Google client +/*.json \ No newline at end of file diff --git a/README.md b/README.md index 405f8a3..f55bb65 100644 --- a/README.md +++ b/README.md @@ -22,87 +22,134 @@ import ( "os" "golang.org/x/oauth2" - "google.golang.org/api/drive/v3" - "sirherobrine23.org/Sirherobrine23/drivefs" + "google.golang.org/api/drive/v2" + "sirherobrine23.com.br/Sirherobrine23/drivefs" ) -var configPath string -var serverPort uint +var ( + configPath = flag.String("config", "./config.json", "Config file path") + serverPort = flag.Uint("port", 8081, "server to listen") + setupAuth = flag.Bool("auth", false, "Listen server and Auth") + + client = flag.String("client", "", "installed.client_id") + secret = flag.String("secret", "", "installed.client_secret") + project = flag.String("project", "", "installed.project_id") + auth_uri = flag.String("auth_uri", "", "installed.auth_uri") + token_uri = flag.String("token_uri", "", "installed.token_uri") + redirect = flag.String("redirect", "", "installed.redirect_uris[]") + access_token = flag.String("access_token", "", "token.access_token") + refresh_token = flag.String("refresh_token", "", "token.refresh_token") + token_type = flag.String("token_type", "", "token.token_type") + root_folder = flag.String("root_folder", "", "Google drive folder id (gdrive:) or path to folder") + + gdriveConfig drivefs.GoogleOauthConfig +) func main() { - flag.StringVar(&configPath, "config", "./config.json", "Config file path") - flag.UintVar(&serverPort, "port", 8081, "server to listen") flag.Parse() + gdriveConfig.Client = *client + gdriveConfig.Secret = *secret + gdriveConfig.Project = *project + gdriveConfig.AuthURI = *auth_uri + gdriveConfig.TokenURI = *token_uri + gdriveConfig.Redirect = *redirect + gdriveConfig.AccessToken = *access_token + gdriveConfig.RefreshToken = *refresh_token + gdriveConfig.TokenType = *token_type + gdriveConfig.RootFolder = *root_folder - var ggdrive *drivefs.Gdrive = new(drivefs.Gdrive) - file, err := os.Open(configPath) - if err != nil { - panic(err) - } - defer file.Close() - if err := json.NewDecoder(file).Decode(ggdrive); err != nil { - panic(err) - } - - if ggdrive.GoogleToken != nil { - var err error - if ggdrive, err = drivefs.New(ggdrive.GoogleOAuth, *ggdrive.GoogleToken); err != nil { - panic(err) + fileConfig, err := os.ReadFile(*configPath) + if err == nil { + if err = json.Unmarshal(fileConfig, &gdriveConfig); err != nil { + fmt.Fprintf(os.Stderr, "Cannot unmarshall config: %s\n", err) + os.Exit(1) + return } + } else if os.IsNotExist(err) { } else { + fmt.Fprintf(os.Stderr, "Cannot open %q: %s\n", *configPath, err) + os.Exit(1) + return + } + + if *setupAuth { ln, err := net.Listen("tcp", ":0") if err != nil { panic(err) } P, _ := netip.ParseAddrPort(ln.Addr().String()) ln.Close() - ggdrive.GoogleOAuth.Redirects = []string{fmt.Sprintf("http://localhost:%d/callback", P.Port())} - config := &oauth2.Config{ClientID: ggdrive.GoogleOAuth.Client, ClientSecret: ggdrive.GoogleOAuth.Secret, RedirectURL: ggdrive.GoogleOAuth.Redirects[0], Scopes: []string{drive.DriveScope, drive.DriveFileScope}, Endpoint: oauth2.Endpoint{AuthURL: ggdrive.GoogleOAuth.AuthURI, TokenURL: ggdrive.GoogleOAuth.TokenURI}} + config := &oauth2.Config{ + ClientID: gdriveConfig.Client, + ClientSecret: gdriveConfig.Secret, + RedirectURL: fmt.Sprintf("http://localhost:%d/callback", P.Port()), + Scopes: []string{drive.DriveScope, drive.DriveFileScope}, + Endpoint: oauth2.Endpoint{ + AuthURL: gdriveConfig.AuthURI, + TokenURL: gdriveConfig.TokenURI, + }, + } - authURL := config.AuthCodeURL("state-token", oauth2.AccessTypeOffline) - fmt.Printf("Go to the following link in your browser then type the authorization code: \n%v\n", authURL) + var ( + server *http.Server + GoogleToken *oauth2.Token + ) - var server *http.Server - var code string mux := http.NewServeMux() - mux.HandleFunc("/callback", func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "text/plain") - w.WriteHeader(200) - w.Write([]byte("Doned\n")) - code = r.URL.Query().Get("code") - if code != "" { - fmt.Printf("Code: %q\n", code) - server.Close() - } + mux.HandleFunc("/token", func(w http.ResponseWriter, r *http.Request) { + authURL := config.AuthCodeURL("state-token", oauth2.AccessTypeOffline) + http.Redirect(w, r, authURL, http.StatusTemporaryRedirect) }) + mux.HandleFunc("/callback", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/plain") + if code := r.URL.Query().Get("code"); code != "" { + if GoogleToken, err = config.Exchange(context.TODO(), code); err != nil { + panic(fmt.Errorf("unable to retrieve token from web %v", err)) + } + + defer server.Close() + w.WriteHeader(200) + fmt.Fprintf(w, "Code: %q", code) + fmt.Printf("Code: %q\n", code) + return + } + w.WriteHeader(400) + w.Write([]byte("Wait to code\n")) + }) + + fmt.Printf("Go to the following link in your browser then type the authorization code: \nhttp://%s/token\n", P.String()) server = &http.Server{Addr: P.String(), Handler: mux} if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { panic(err) } - ggdrive.GoogleToken, err = config.Exchange(context.TODO(), code) + gdriveConfig.AccessToken = GoogleToken.AccessToken + gdriveConfig.RefreshToken = GoogleToken.RefreshToken + gdriveConfig.TokenType = GoogleToken.TokenType + gdriveConfig.Expire = GoogleToken.Expiry + + data, err := json.MarshalIndent(gdriveConfig, "", " ") if err != nil { - panic(fmt.Errorf("unable to retrieve token from web %v", err)) - } - - file.Close() - if file, err = os.Create(configPath); err != nil { panic(err) - } - - at := json.NewEncoder(file) - at.SetIndent("", " ") - if err := at.Encode(ggdrive); err != nil { + } else if err = os.WriteFile(*configPath, data, 0666); err != nil { panic(err) } } - fmt.Printf("server listening on :%d\n", serverPort) - if err := http.ListenAndServe(fmt.Sprintf(":%d", serverPort), http.FileServerFS(ggdrive)); err != nil { + gdrive, err := drivefs.NewGoogleDrive(gdriveConfig) + if err != nil { + fmt.Fprintf(os.Stderr, "Cannot open gdrive client: %s\n", err) + os.Exit(1) + return + } + + fmt.Printf("server listening on :%d\n", *serverPort) + if err := http.ListenAndServe(fmt.Sprintf(":%d", *serverPort), http.FileServerFS(gdrive)); err != nil { fmt.Fprintln(os.Stderr, err.Error()) os.Exit(1) } + } ``` \ No newline at end of file diff --git a/cache/cache.go b/cache/cache.go index 440c27a..05399a2 100644 --- a/cache/cache.go +++ b/cache/cache.go @@ -1,72 +1,65 @@ package cache import ( - "sync" + "encoding" + "encoding/json" + "errors" + "iter" "time" ) -type cacheInfo[D any] struct { - TimeValid time.Time // Valid time - Data D // Data +var ( + ErrNotExist error = errors.New("key not exists") +) + +// Generic Cache interface +type Cache[T any] interface { + Delete(key string) error // Remove value from cache + Set(ttl time.Duration, key string, value T) error // set new value or replace current value + Get(key string) (T, error) // Get current value + Values() iter.Seq2[string, T] // List all keys with u values + Flush() error // Remove all outdated values } -type LocalCache[T any] struct { - l map[string]*cacheInfo[T] - - rw sync.Mutex -} - -// Get value from Key -func (w *LocalCache[T]) Get(Key string) (T, bool) { - if len(w.l) == 0 { - return *new(T), false - } - - w.rw.Lock() - defer w.rw.Unlock() - data, ok := w.l[Key] - if ok { - if data.TimeValid.Unix() >= time.Now().Unix() { - delete(w.l, Key) - return *new(T), false +func ToString(v any) (string, error) { + switch v := v.(type) { + case encoding.TextMarshaler: + data, err := v.MarshalText() + if err != nil { + return "", err } - return data.Data, true - } - return *new(T), false -} - -// Set value to cache struct -func (w *LocalCache[T]) Set(ValidAt time.Time, Key string, Value T) { - w.rw.Lock() - defer w.rw.Unlock() - if len(w.l) == 0 { - w.l = make(map[string]*cacheInfo[T]) - } - - w.l[Key] = &cacheInfo[T]{ - TimeValid: ValidAt, - Data: Value, - } -} - -// Delete key if exists -func (w *LocalCache[T]) Delete(Key string) { - w.rw.Lock() - defer w.rw.Unlock() - delete(w.l, Key) -} - -// Remove expired Cache -func (w *LocalCache[T]) Flush() int { - w.rw.Lock() - defer w.rw.Unlock() - - flushed, now := 0, time.Now().Unix() - for key, data := range w.l { - if data.TimeValid.Unix() >= now { - delete(w.l, key) - flushed++ + return string(data), nil + case encoding.BinaryMarshaler: + data, err := v.MarshalBinary() + if err != nil { + return "", err } + return string(data), nil + case json.Marshaler: + data, err := v.MarshalJSON() + if err != nil { + return "", err + } + return string(data), nil + default: + data, err := json.Marshal(v) + if err != nil { + return "", err + } + return string(data), nil } - return flushed +} + +func FromString[T any](value string) (target T, err error) { + switch v := any(target).(type) { + case encoding.TextUnmarshaler: + err = v.UnmarshalText([]byte(value)) + case encoding.BinaryUnmarshaler: + err = v.UnmarshalBinary([]byte(value)) + case json.Unmarshaler: + err = v.UnmarshalJSON([]byte(value)) + default: + err = json.Unmarshal([]byte(value), &value) + } + return } diff --git a/cache/db.go b/cache/db.go new file mode 100644 index 0000000..38a625a --- /dev/null +++ b/cache/db.go @@ -0,0 +1,156 @@ +package cache + +import ( + "database/sql" + "database/sql/driver" + "fmt" + "iter" + "time" +) + +type DBColl struct { + ID sql.NullInt64 + Key sql.NullString + TimeTTL sql.NullTime + Value sql.NullString +} + +type Database[T any] struct { + DBName string + DB *sql.DB +} + +// Open new connection with [database/sql.Open] and attach +func NewOpenDB[T any](DriveName, dataSourceName, DBName string) (Cache[T], error) { + db, err := sql.Open(DriveName, dataSourceName) + if err != nil { + return nil, err + } + ndb := &Database[T]{DBName: DBName, DB: db} + if err := ndb.CreateTable(); err != nil { + return nil, err + } + return ndb, nil +} + +// Attach in db with [database/sql/driver.Connector] +func NewOpenConnectorDB[T any](drive driver.Connector, DBName string) (Cache[T], error) { + ndb := &Database[T]{DBName: DBName, DB: sql.OpenDB(drive)} + if err := ndb.CreateTable(); err != nil { + return nil, err + } + return ndb, nil +} + +// Attach connection with current [database/sql.DB] +func NewAttachDB[T any](db *sql.DB, DBName string) (Cache[T], error) { + ndb := &Database[T]{DBName: DBName, DB: db} + if err := ndb.CreateTable(); err != nil { + return nil, err + } + return ndb, nil +} + +func (db *Database[T]) CreateTable() error { + q, err := db.DB.Query(`SELECT * FROM ?`, db.DBName) + if err == nil { + q.Close() + return nil + } + _, err = db.DB.Exec(fmt.Sprintf(`CREATE TABLE IF NOT EXISTS %q ( + Key TEXT UNIQUE NOT NULL, + TTL DATETIME, + MsgValue TEXT, + PRIMARY KEY (Key) + )`, db.DBName)) + return err +} + +func (db *Database[T]) Flush() error { + _, err := db.DB.Exec(fmt.Sprintf(`DELETE FROM %q WHERE (TTL < ?)`, db.DBName), sql.NullTime{Valid: true, Time: time.Now()}) + if err == sql.ErrNoRows { + err = nil + } + return err +} + +func (db *Database[T]) Delete(key string) error { + _, err := db.DB.Exec(fmt.Sprintf(`DELETE FROM %q WHERE Key == ?`, db.DBName), key) + return err +} + +func (db *Database[T]) Get(key string) (value T, err error) { + var Value sql.NullString + if err := db.DB.QueryRow(fmt.Sprintf(`SELECT MsgValue FROM %q WHERE Key == ?`, db.DBName), key).Scan(&Value); err != nil { + if err == sql.ErrNoRows { + return *new(T), ErrNotExist + } + return *new(T), err + } else if !Value.Valid { + return *new(T), ErrNotExist + } + return FromString[T](Value.String) +} + +func (db *Database[T]) Set(ttl time.Duration, key string, value T) (err error) { + var ( + KeyStr, MsgValue sql.NullString + TTL sql.NullTime + ) + + KeyStr.Valid, MsgValue.Valid, TTL.Valid = true, true, true + KeyStr.String = key + TTL.Time = time.Now().Add(ttl) + if MsgValue.String, err = ToString(value); err != nil { + return + } + + if err = db.DB.QueryRow(fmt.Sprintf(`SELECT Key FROM %q WHERE Key == %q`, db.DBName, key)).Scan(&key); err != nil { + if err != sql.ErrNoRows { + return err + } + if _, err = db.DB.Exec(fmt.Sprintf(`INSERT INTO %q (Key, TTL, MsgValue) VALUES (?,?,?)`, db.DBName), KeyStr, TTL, MsgValue); !(err == nil || err == sql.ErrNoRows) { + return nil + } + return nil + } + + _, err = db.DB.Exec(fmt.Sprintf(`UPDATE %q SET MsgValue = ?, TTL = ? WHERE Key == ?`, db.DBName), MsgValue, TTL, KeyStr) + if err != nil { + return nil + } + return nil +} + +func (db *Database[T]) Values() iter.Seq2[string, T] { + return func(yield func(string, T) bool) { + rows, err := db.DB.Query(fmt.Sprintf(`SELECT (Key, TTL, MsgValue) FROM %s`, db.DBName)) + if err != nil { + panic(err) + } + defer rows.Close() + for rows.Next() { + var ( + Key, MsgValue sql.NullString + TTL sql.NullTime + ) + if err = rows.Scan(&Key, &TTL, &MsgValue); err != nil { + panic(err) + } + if TTL.Time.Compare(time.Now()) != 1 { + continue + } + + value, err := FromString[T](MsgValue.String) + if err != nil { + panic(err) + } + if !yield(Key.String, value) { + return + } + } + if err = rows.Err(); err != nil { + panic(err) + } + } +} diff --git a/cache/db_test.go b/cache/db_test.go new file mode 100644 index 0000000..5694c29 --- /dev/null +++ b/cache/db_test.go @@ -0,0 +1,57 @@ +package cache + +import ( + "fmt" + "testing" + "time" + + _ "modernc.org/sqlite" +) + +type Value struct { + Title string `json:"title"` + Msg string `json:"msg"` +} + +func TestDbSqlite(t *testing.T) { + cache, err := NewOpenDB[Value]("sqlite", "../cache_test.db", "cache") + if err != nil { + t.Skip(err) + return + } + + fistValue := Value{ + Title: "Google", + Msg: "made by golang.", + } + + if err := cache.Set(time.Hour, "fist1", fistValue); err != nil { + t.Error(fmt.Errorf("cannot set fist1: %s", err)) + return + } + + // Invalid method + if err := cache.Set(0, "fist2", fistValue); err != nil { + t.Error(fmt.Errorf("cannot set fist2: %s", err)) + return + } + + recoveryValue, err := cache.Get("fist1") + if err != nil { + t.Error(fmt.Errorf("cannot get fist1: %s", err)) + return + } + + if fistValue.Title != recoveryValue.Title { + t.Errorf("Title is not same: %q != %q", fistValue.Title, recoveryValue.Title) + return + } else if fistValue.Msg != recoveryValue.Msg { + t.Errorf("Msg is not same: %q != %q", fistValue.Msg, recoveryValue.Msg) + return + } + + if err := cache.Flush(); err != nil { + t.Errorf("cannot flush: %s", err) + return + } +} diff --git a/cache/memory.go b/cache/memory.go new file mode 100644 index 0000000..9f058c2 --- /dev/null +++ b/cache/memory.go @@ -0,0 +1,70 @@ +package cache + +import ( + "iter" + "sync" + "time" +) + +type MemoryValue[T any] struct { + ValidTime time.Time + Value T +} + +type Memory[T any] struct { + Vmap map[string]*MemoryValue[T] // Memory values + locker sync.RWMutex // sync to map +} + +func NewMemory[T any]() Cache[T] { + return &Memory[T]{map[string]*MemoryValue[T]{}, sync.RWMutex{}} +} + +func (mem *Memory[T]) Delete(key string) error { + mem.locker.Lock() + defer mem.locker.Unlock() + delete(mem.Vmap, key) + return nil +} + +func (mem *Memory[T]) Set(ttl time.Duration, key string, value T) error { + mem.locker.Lock() + defer mem.locker.Unlock() + mem.Vmap[key] = &MemoryValue[T]{time.Now().Add(ttl), value} + return nil +} + +func (mem *Memory[T]) Get(key string) (value T, err error) { + mem.locker.RLock() + defer mem.locker.RUnlock() + if v, ok := mem.Vmap[key]; ok && v != nil && v.ValidTime.Compare(time.Now()) != 1 { + return v.Value, nil + } + return +} + +func (mem *Memory[T]) Values() iter.Seq2[string, T] { + return func(yield func(string, T) bool) { + mem.locker.RLock() + defer mem.locker.RUnlock() + for key, value := range mem.Vmap { + if value == nil || value.ValidTime.Compare(time.Now()) != 1 { + continue + } + if !yield(key, value.Value) { + return + } + } + } +} + +func (mem *Memory[T]) Flush() error { + mem.locker.Lock() + defer mem.locker.Unlock() + for key, value := range mem.Vmap { + if value == nil || value.ValidTime.Compare(time.Now()) != 1 { + delete(mem.Vmap, key) + } + } + return nil +} diff --git a/cache/valkey.go b/cache/valkey.go new file mode 100644 index 0000000..fff2758 --- /dev/null +++ b/cache/valkey.go @@ -0,0 +1,44 @@ +package cache + +import ( + "context" + "iter" + "time" + + "github.com/valkey-io/valkey-go" +) + +type Valkey[T any] struct { + Client valkey.Client +} + +func NewValkey[T any](opt valkey.ClientOption) (Cache[T], error) { + client, err := valkey.NewClient(opt) + if err != nil { + return nil, err + } + return Valkey[T]{Client: client}, nil +} + +func (valkey Valkey[T]) Flush() error { return nil } +func (valkey Valkey[T]) Values() iter.Seq2[string, T] { return nil } + +func (valkey Valkey[T]) Delete(key string) error { + return valkey.Client.Do(context.Background(), valkey.Client.B().Del().Key(key).Build()).Error() +} + +func (valkey Valkey[T]) Set(ttl time.Duration, key string, value T) error { + data, err := ToString(value) + if err != nil { + return err + } + return valkey.Client.Do(context.Background(), valkey.Client.B().Set().Key(key).Value(data).Ex(ttl).Build()).Error() +} + +func (valkey Valkey[T]) Get(key string) (T, error) { + str, err := valkey.Client.Do(context.Background(), valkey.Client.B().Get().Key(key).Build()).ToString() + if err != nil { + return *new(T), err + } + return FromString[T](str) +} diff --git a/example/main.go b/example/main.go new file mode 100644 index 0000000..ef69d52 --- /dev/null +++ b/example/main.go @@ -0,0 +1,143 @@ +package main + +import ( + "context" + "encoding/json" + "flag" + "fmt" + "net" + "net/http" + "net/netip" + "os" + + "golang.org/x/oauth2" + "google.golang.org/api/drive/v2" + "sirherobrine23.com.br/Sirherobrine23/drivefs" +) + +var ( + configPath = flag.String("config", "./config.json", "Config file path") + serverPort = flag.Uint("port", 8081, "server to listen") + setupAuth = flag.Bool("auth", false, "Listen server and Auth") + + client = flag.String("client", "", "installed.client_id") + secret = flag.String("secret", "", "installed.client_secret") + project = flag.String("project", "", "installed.project_id") + auth_uri = flag.String("auth_uri", "", "installed.auth_uri") + token_uri = flag.String("token_uri", "", "installed.token_uri") + redirect = flag.String("redirect", "", "installed.redirect_uris[]") + access_token = flag.String("access_token", "", "token.access_token") + refresh_token = flag.String("refresh_token", "", "token.refresh_token") + token_type = flag.String("token_type", "", "token.token_type") + root_folder = flag.String("root_folder", "", "Google drive folder id (gdrive:) or path to folder") + + gdriveConfig drivefs.GoogleOauthConfig +) + +func main() { + flag.Parse() + gdriveConfig.Client = *client + gdriveConfig.Secret = *secret + gdriveConfig.Project = *project + gdriveConfig.AuthURI = *auth_uri + gdriveConfig.TokenURI = *token_uri + gdriveConfig.Redirect = *redirect + gdriveConfig.AccessToken = *access_token + gdriveConfig.RefreshToken = *refresh_token + gdriveConfig.TokenType = *token_type + gdriveConfig.RootFolder = *root_folder + + fileConfig, err := os.ReadFile(*configPath) + if err == nil { + if err = json.Unmarshal(fileConfig, &gdriveConfig); err != nil { + fmt.Fprintf(os.Stderr, "Cannot unmarshall config: %s\n", err) + os.Exit(1) + return + } + } else if os.IsNotExist(err) { + } else { + fmt.Fprintf(os.Stderr, "Cannot open %q: %s\n", *configPath, err) + os.Exit(1) + return + } + + if *setupAuth { + ln, err := net.Listen("tcp", ":0") + if err != nil { + panic(err) + } + P, _ := netip.ParseAddrPort(ln.Addr().String()) + ln.Close() + + config := &oauth2.Config{ + ClientID: gdriveConfig.Client, + ClientSecret: gdriveConfig.Secret, + RedirectURL: fmt.Sprintf("http://localhost:%d/callback", P.Port()), + Scopes: []string{drive.DriveScope, drive.DriveFileScope}, + Endpoint: oauth2.Endpoint{ + AuthURL: gdriveConfig.AuthURI, + TokenURL: gdriveConfig.TokenURI, + }, + } + + var ( + server *http.Server + GoogleToken *oauth2.Token + ) + + mux := http.NewServeMux() + mux.HandleFunc("/token", func(w http.ResponseWriter, r *http.Request) { + authURL := config.AuthCodeURL("state-token", oauth2.AccessTypeOffline) + http.Redirect(w, r, authURL, http.StatusTemporaryRedirect) + }) + + mux.HandleFunc("/callback", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/plain") + if code := r.URL.Query().Get("code"); code != "" { + if GoogleToken, err = config.Exchange(context.TODO(), code); err != nil { + panic(fmt.Errorf("unable to retrieve token from web %v", err)) + } + + defer server.Close() + w.WriteHeader(200) + fmt.Fprintf(w, "Code: %q", code) + fmt.Printf("Code: %q\n", code) + return + } + w.WriteHeader(400) + w.Write([]byte("Wait to code\n")) + }) + + fmt.Printf("Go to the following link in your browser then type the authorization code: \nhttp://%s/token\n", P.String()) + server = &http.Server{Addr: P.String(), Handler: mux} + if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { + panic(err) + } + + gdriveConfig.AccessToken = GoogleToken.AccessToken + gdriveConfig.RefreshToken = GoogleToken.RefreshToken + gdriveConfig.TokenType = GoogleToken.TokenType + gdriveConfig.Expire = GoogleToken.Expiry + + data, err := json.MarshalIndent(gdriveConfig, "", " ") + if err != nil { + panic(err) + } else if err = os.WriteFile(*configPath, data, 0666); err != nil { + panic(err) + } + } + + gdrive, err := drivefs.NewGoogleDrive(gdriveConfig) + if err != nil { + fmt.Fprintf(os.Stderr, "Cannot open gdrive client: %s\n", err) + os.Exit(1) + return + } + + fmt.Printf("server listening on :%d\n", *serverPort) + if err := http.ListenAndServe(fmt.Sprintf(":%d", *serverPort), http.FileServerFS(gdrive)); err != nil { + fmt.Fprintln(os.Stderr, err.Error()) + os.Exit(1) + } + +} diff --git a/file.go b/file.go new file mode 100644 index 0000000..0f1d42d --- /dev/null +++ b/file.go @@ -0,0 +1,356 @@ +package drivefs + +import ( + "bytes" + "errors" + "fmt" + "io" + "io/fs" + "net/http" + "path" + "strings" + "time" + + "google.golang.org/api/drive/v3" +) + +func escapeName(n string) string { + return strings.Join(strings.Split(n, "/"), "%%2f") +} + +// Extends [*google.golang.org/api/drive/v3.File] +type Stat struct{ File *drive.File } + +func (node Stat) Sys() any { return node.File } +func (node Stat) Name() string { return escapeName(path.Clean(node.File.Name)) } +func (node Stat) Size() int64 { return node.File.Size } +func (node Stat) IsDir() bool { return node.File.MimeType == GoogleDriveMimeFolder } +func (node Stat) Mode() fs.FileMode { + switch node.File.MimeType { + case GoogleDriveMimeFolder: + return fs.ModeDir | fs.ModePerm + case GoogleDriveMimeSyslink: + return fs.ModeSymlink | fs.ModePerm + default: + return fs.ModePerm + } +} + +func (node Stat) ModTime() time.Time { + t := time.Date(0, 0, 0, 0, 0, 0, 0, time.UTC) + for _, fileTime := range []string{node.File.ModifiedTime, node.File.CreatedTime} { + if fileTime == "" { + continue + } + t.UnmarshalText([]byte(fileTime)) + break + } + return t +} + +var ( + _ File = (*GdriveNode)(nil) + _ fs.File = (File)(nil) + + ErrInvalidOffset error = errors.New("Seek: invalid offset") +) + +type File interface { + io.ReadWriteCloser + io.ReaderFrom + io.WriterTo + Stat() (fs.FileInfo, error) + ReadDir(count int) ([]fs.DirEntry, error) +} + +const ( + DirectionWait Direction = iota // Wait to read or write + DirectionWrite // Only accept write + DirectionReader // Only accept reader +) + +type Direction int + +type ErrInvalidDirection Direction + +func (err ErrInvalidDirection) Error() string { + switch Direction(err) { + case DirectionReader: + return "cannot access write with direction is writer" + case DirectionWrite: + return "cannot write with direction is reader" + case DirectionWait: + return "cannot write or reader without open fist file" + } + return "unknown direction" +} + +type GdriveNode struct { + filename string // Filename path + gClient *Gdrive // Client setuped + node *drive.File // File node + nodeRoot *drive.File // root to create file + sRead *io.PipeReader // Pipe reader + sWrite *io.PipeWriter // Pipe writer + offset int64 // File offset + sReadRes *http.Response // read http response if is read operation + direction Direction // File direction + + // Files in node + + filesOffset int // current count + nodeFiles []fs.DirEntry // files node +} + +func (node GdriveNode) Stat() (fs.FileInfo, error) { + if node.node == nil { + return nil, fs.ErrNotExist + } + return &Stat{File: node.node}, nil +} + +func (node *GdriveNode) ReadFrom(r io.Reader) (n int64, err error) { + if len(node.nodeFiles) > 0 || node.filesOffset > 0 { + return 0, &fs.PathError{Op: "readfrom", Path: node.filename, Err: fs.ErrInvalid} + } + pathNodes := node.gClient.pathSplit(node.filename) + if !(node.direction == DirectionWrite || node.direction == DirectionWait) { + return 0, fs.ErrInvalid + } + + // Copy from current offset + if node.direction == DirectionWrite { + return io.Copy(node, r) + } + + rootSolver := node.gClient.rootDrive + if node.node == nil && node.nodeRoot == nil { + if node.gClient.checkMkdir(node.filename) { + if rootSolver, err = node.gClient.mkdirAllNodes(pathNodes[len(pathNodes)-2].Path); err != nil { + return 0, err + } + } + + if rootSolver, err = node.gClient.driveService.Files.Create(&drive.File{MimeType: "application/octet-stream", Name: pathNodes[len(pathNodes)-1].Name, Parents: []string{rootSolver.Id}}).Fields("*").Media(r).Do(); err != nil { + return 0, err + } + node.node = rootSolver // set new node + } else if node.node == nil && node.nodeRoot != nil { + if rootSolver, err = node.gClient.driveService.Files.Create(&drive.File{MimeType: "application/octet-stream", Name: pathNodes[len(pathNodes)-1].Name, Parents: []string{node.nodeRoot.Id}}).Fields("*").Media(r).Do(); err != nil { + return 0, err + } + node.node = rootSolver // set new node + } else if rootSolver, err = node.gClient.driveService.Files.Update(node.node.Id, nil).Media(r).Do(); err != nil { + return 0, err + } + + node.gClient.cachePut(pathNodes[len(pathNodes)-1].Path, rootSolver) + return rootSolver.Size, nil +} + +func (node *GdriveNode) WriteTo(w io.Writer) (n int64, err error) { + if len(node.nodeFiles) > 0 || node.filesOffset > 0 { + return 0, &fs.PathError{Op: "writeto", Path: node.filename, Err: fs.ErrInvalid} + } + if node.node == nil { + return 0, fs.ErrNotExist + } else if !(node.direction == DirectionReader || node.direction == DirectionWait) { + return 0, fs.ErrInvalid + } + + // Write from current offset + if node.direction == DirectionReader { + return io.Copy(w, node) + } + + res, err := node.gClient.getRequest(node.gClient.driveService.Files.Get(node.node.Id)) + if err != nil { + return 0, err + } + defer res.Body.Close() + return io.Copy(w, res.Body) +} + +func (node *GdriveNode) Close() error { + switch node.direction { + case DirectionWrite: + if node.sWrite != nil { + if err := node.sWrite.Close(); err != nil { + return err + } + } + case DirectionReader: + if node.sReadRes != nil { + if err := node.sReadRes.Body.Close(); err != nil { + return err + } + } + if node.sRead != nil { + if err := node.sRead.Close(); err != nil { + return err + } + } + } + node.direction = DirectionWait + return nil +} + +func (node *GdriveNode) Read(p []byte) (n int, err error) { + if len(node.nodeFiles) > 0 || node.filesOffset > 0 { + return 0, &fs.PathError{Op: "read", Path: node.filename, Err: fs.ErrInvalid} + } + err = io.EOF // default error + switch node.direction { + case DirectionWrite: + return 0, ErrInvalidDirection(DirectionReader) + case DirectionReader: + if node.sReadRes != nil { + n, err = node.sReadRes.Body.Read(p) + } else if node.sRead != nil { + n, err = node.sRead.Read(p) + } + case DirectionWait: + if node.node == nil { + return 0, io.ErrUnexpectedEOF + } + if node.sReadRes, err = node.gClient.getRequest(node.gClient.driveService.Files.Get(node.node.Id)); err != nil { + return 0, err + } + node.direction = DirectionReader + n, err = node.sReadRes.Body.Read(p) + } + + node.offset += int64(n) + return +} + +func (node *GdriveNode) Write(p []byte) (n int, err error) { + if len(node.nodeFiles) > 0 || node.filesOffset > 0 { + return 0, &fs.PathError{Op: "write", Path: node.filename, Err: fs.ErrInvalid} + } + err = io.EOF // default error + switch node.direction { + case DirectionReader: + return 0, ErrInvalidDirection(DirectionWrite) + case DirectionWrite: + if node.sWrite != nil { + n, err = node.sWrite.Write(p) + } + case DirectionWait: + node.direction = DirectionWrite + pathNodes := node.gClient.pathSplit(node.filename) + nodeID := "" + + if node.node == nil && node.nodeRoot == nil { + if node.gClient.checkMkdir(node.filename) { + if node.nodeRoot, err = node.gClient.mkdirAllNodes(pathNodes[len(pathNodes)-2].Path); err != nil { + return 0, err + } + } + } + if node.node == nil { + node.sRead, node.sWrite = io.Pipe() + if node.node, err = node.gClient.driveService.Files.Create(&drive.File{MimeType: "application/octet-stream", Name: pathNodes[len(pathNodes)-1].Name, Parents: []string{node.nodeRoot.Id}}).Fields("*").Media(bytes.NewReader([]byte{})).Do(); err != nil { + return 0, err + } + } + + nodeID = node.node.Id + go node.gClient.driveService.Files.Update(nodeID, nil).Media(node.sRead).Do() + n, err = node.sWrite.Write(p) + } + + node.offset += int64(n) // append new offset + return +} + +func (node *GdriveNode) Seek(offset int64, whence int) (of int64, err error) { + if len(node.nodeFiles) > 0 || node.filesOffset > 0 { + return 0, &fs.PathError{Op: "seek", Path: node.filename, Err: fs.ErrInvalid} + } + switch node.direction { + case DirectionWait: + if !(whence == io.SeekStart || whence == io.SeekCurrent) { + return 0, io.ErrUnexpectedEOF + } else if offset < 0 { + return 0, &fs.PathError{Op: "seek", Path: node.filename, Err: fs.ErrInvalid} + } + node.offset = offset + return offset, nil + case DirectionReader: + switch whence { + case io.SeekCurrent: + return io.CopyN(io.Discard, node, offset) + case io.SeekStart: + if offset < 0 || offset > node.node.Size { + return 0, &fs.PathError{Op: "seek", Path: node.filename, Err: fs.ErrInvalid} + } + + // Close current body + if node.sReadRes != nil { + if err = node.sReadRes.Body.Close(); err != nil { + return 0, &fs.PathError{Op: "seek", Path: node.filename, Err: err} + } + node.sReadRes = nil + } + + fileCall := node.gClient.driveService.Files.Get(node.node.Id) + if offset > 0 { + fileCall.Header().Set("Range", fmt.Sprintf("bytes=%d-%d", offset, node.node.Size-1)) + } + + if node.sReadRes, err = node.gClient.getRequest(fileCall); err != nil { + return 0, err + } + node.offset = offset + case io.SeekEnd: + newOffset := node.node.Size - offset + if newOffset < 0 { + return 0, &fs.PathError{Op: "seek", Path: node.filename, Err: fs.ErrInvalid} + } + + fileCall := node.gClient.driveService.Files.Get(node.node.Id) + fileCall.Header().Set("Range", fmt.Sprintf("bytes=%d-%d", newOffset, node.node.Size-1)) + if node.sReadRes, err = node.gClient.getRequest(fileCall); err != nil { + return 0, &fs.PathError{Op: "seek", Path: node.filename, Err: err} + } + node.offset = newOffset + return newOffset, nil + } + case DirectionWrite: + switch whence { + case io.SeekCurrent: + of = 0 + for offset > 0 { + buffSize := min(4028, offset) + offset -= buffSize + n, err := node.sWrite.Write(make([]byte, buffSize)) + if err != nil { + return 0, &fs.PathError{Op: "seek", Path: node.filename, Err: err} + } + of += int64(n) + } + case io.SeekStart, io.SeekEnd: + return 0, &fs.PathError{Op: "seek", Path: node.filename, Err: fs.ErrInvalid} + } + } + return 0, io.EOF +} + +// cannot list files nodes +func (node *GdriveNode) ReadDir(count int) (entrys []fs.DirEntry, err error) { + if len(node.nodeFiles) == 0 && node.filesOffset == 0 { + return nil, &fs.PathError{Op: "readdir", Path: node.filename, Err: fs.ErrInvalid} + } else if len(node.nodeFiles) == 0 { + return nil, io.EOF + } else if count == -1 { + entrys = node.nodeFiles + node.nodeFiles = nil + return + } + + count = min(len(node.nodeFiles), count) + entrys = node.nodeFiles[:count] + node.nodeFiles = node.nodeFiles[count:] + return +} diff --git a/gdrive.go b/gdrive.go index 6b30b07..f39d580 100644 --- a/gdrive.go +++ b/gdrive.go @@ -3,12 +3,17 @@ package drivefs import ( "context" "fmt" - "io" "io/fs" + "net/http" + "net/url" + "path" "path/filepath" + "reflect" + "slices" "strings" "time" + "golang.org/x/net/http2" "golang.org/x/oauth2" "google.golang.org/api/drive/v3" "google.golang.org/api/option" @@ -28,6 +33,22 @@ var ( _ fs.ReadDirFS = &Gdrive{} _ fs.ReadFileFS = &Gdrive{} _ fs.SubFS = &Gdrive{} + + GDocsMime = []string{ + "application/vnd.google-apps.document", + "application/vnd.google-apps.drive-sdk", + "application/vnd.google-apps.drawing", + "application/vnd.google-apps.form", + "application/vnd.google-apps.fusiontable", + "application/vnd.google-apps.jam", + "application/vnd.google-apps.mail-layout", + "application/vnd.google-apps.map", + "application/vnd.google-apps.presentation", + "application/vnd.google-apps.script", + "application/vnd.google-apps.site", + "application/vnd.google-apps.spreadsheet", + "application/vnd.google-apps.unknown", + } ) type Fs interface { @@ -36,37 +57,42 @@ type Fs interface { fs.ReadDirFS fs.ReadFileFS fs.SubFS - fs.GlobFS } type Gdrive struct { - GoogleConfig *oauth2.Config // Google client app oauth project - GoogleToken *oauth2.Token // Authenticated user - driveService *drive.Service // Google drive service - rootDrive *drive.File // Root to find files + GoogleConfig *oauth2.Config `json:"client"` // Google client app oauth project + GoogleToken *oauth2.Token `json:"token"` // Authenticated user - cache *cache.LocalCache[*drive.File] + driveService *drive.Service // Google drive service + rootDrive *drive.File // Root to find files + cache cache.Cache[*drive.File] // Cache struct } // GoogleOauthConfig represents google oauth token for drive setup type GoogleOauthConfig struct { - Client string `json:",omitempty"` - Secret string `json:",omitempty"` - Project string `json:",omitempty"` - AuthURI string `json:",omitempty"` - TokenURI string `json:",omitempty"` - Redirect string `json:",omitempty"` - AccessToken string `json:",omitempty"` - RefreshToken string `json:",omitempty"` - Expire time.Time `json:",omitempty"` - TokenType string `json:",omitempty"` - RootFolder string `json:",omitempty"` // Google drive folder id (gdrive:) or path to folder + Client string `json:"client,omitempty"` // installed.client_id + Secret string `json:"secret,omitempty"` // installed.client_secret + Project string `json:"project,omitempty"` // installed.project_id + AuthURI string `json:"auth_uri,omitempty"` // installed.auth_uri + TokenURI string `json:"token_uri,omitempty"` // installed.token_uri + Redirect string `json:"redirect,omitempty"` // installed.redirect_uris[] + AccessToken string `json:"access_token,omitempty"` // token.access_token + RefreshToken string `json:"refresh_token,omitempty"` // token.refresh_token + TokenType string `json:"token_type,omitempty"` // token.token_type + Expire time.Time `json:"expire,omitzero"` // token.expiry + RootFolder string `json:"root_folder,omitempty"` // Google drive folder id (gdrive:) or path to folder + Cacher cache.Cache[*drive.File] `json:"-"` // Cache struct } // Create new Gdrive struct and configure google drive client -func NewGoogleDrive(config GoogleOauthConfig) (*Gdrive, error) { +func NewGoogleDrive(config GoogleOauthConfig) (Fs, error) { + // Make cache in memory if not set cache + if config.Cacher == nil { + config.Cacher = cache.NewMemory[*drive.File]() + } + gdrive := &Gdrive{ - cache: &cache.LocalCache[*drive.File]{}, + cache: config.Cacher, GoogleConfig: &oauth2.Config{ ClientID: config.Client, ClientSecret: config.Secret, @@ -98,13 +124,13 @@ func NewGoogleDrive(config GoogleOauthConfig) (*Gdrive, error) { return nil, fmt.Errorf("cannot get root: %v", err) } n = n[1:] - } else if gdrive.rootDrive, err = gdrive.MkdirAll(strings.Join(n, "/")); err != nil { + } else if gdrive.rootDrive, err = gdrive.mkdirAllNodes(strings.Join(n, "/")); err != nil { return nil, err } // resolve and create path not exists in new root if len(n) >= 1 { - if gdrive.rootDrive, err = gdrive.MkdirAll(strings.Join(n, "/")); err != nil { + if gdrive.rootDrive, err = gdrive.mkdirAllNodes(strings.Join(n, "/")); err != nil { return nil, err } } @@ -116,15 +142,15 @@ func NewGoogleDrive(config GoogleOauthConfig) (*Gdrive, error) { } func (gdrive *Gdrive) cacheDelete(path string) { - gdrive.cache.Delete(fmt.Sprintf("gdrive:%s:%s", gdrive.rootDrive.Id, gdrive.fixPath(path))) + gdrive.cache.Delete(fmt.Sprintf("gdrive:%q:%s", gdrive.fixPath(path), gdrive.rootDrive.Id)) } func (gdrive *Gdrive) cachePut(path string, node *drive.File) { - gdrive.cache.Set(time.Now().Add(time.Hour), fmt.Sprintf("gdrive:%s:%s", gdrive.rootDrive.Id, gdrive.fixPath(path)), node) + gdrive.cache.Set(time.Hour, fmt.Sprintf("gdrive:%s:%s", gdrive.rootDrive.Id, gdrive.fixPath(path)), node) } func (gdrive *Gdrive) cacheGet(path string) *drive.File { - if node, ok := gdrive.cache.Get(fmt.Sprintf("gdrive:%s:%s", gdrive.rootDrive.Id, gdrive.fixPath(path))); ok && node != nil { + if node, err := gdrive.cache.Get(fmt.Sprintf("gdrive:%s:%s", gdrive.rootDrive.Id, gdrive.fixPath(path))); err == nil && node != nil { return node } return nil @@ -132,6 +158,10 @@ func (gdrive *Gdrive) cacheGet(path string) *drive.File { // Get Node info and is not trashed/deleted func (gdrive *Gdrive) resolveNode(folderID, name string) (*drive.File, error) { + if name == "." || name == "/" { + return gdrive.rootDrive, nil + } + name = strings.ReplaceAll(strings.ReplaceAll(name, `\`, `\\`), `'`, `\'`) file, err := gdrive.driveService.Files.List().Fields("*").PageSize(300).Q(fmt.Sprintf(GoogleListQueryWithName, folderID, name)).Do() if err != nil { @@ -155,7 +185,13 @@ func (gdrive *Gdrive) listNodes(folderID string) ([]*drive.File, error) { if err != nil { return nodes, err } - nodes = append(nodes, res.Files...) + + for nodeIndex := range res.Files { + if !slices.Contains(GDocsMime, res.Files[nodeIndex].MimeType) { + nodes = append(nodes, res.Files[nodeIndex]) + } + } + if folderGdrive.PageToken(res.NextPageToken); res.NextPageToken == "" { break } @@ -163,23 +199,6 @@ func (gdrive *Gdrive) listNodes(folderID string) ([]*drive.File, error) { return nodes, nil } -// Resolve node path and return New Gdrive struct -func (gdrive *Gdrive) Sub(dir string) (fs.FS, error) { - node, err := gdrive.resolveNode(gdrive.rootDrive.Id, dir) - if err != nil { - return nil, err - } - - // Return New gdrive struct - return &Gdrive{ - cache: gdrive.cache, - driveService: gdrive.driveService, - GoogleConfig: gdrive.GoogleConfig, - GoogleToken: gdrive.GoogleToken, - rootDrive: node, - }, nil -} - // Split to nodes func (*Gdrive) pathSplit(path string) []struct{ Name, Path string } { path = strings.Trim(filepath.ToSlash(path), "/") @@ -208,6 +227,59 @@ func (gdrive *Gdrive) getLast(path string) struct{ Name, Path string } { return n[len(n)-1] } +// Resolve node path from last node to fist/root path +func (gdrive *Gdrive) forwardPathResove(nodeID string) (string, error) { + pathNodes, fistNode, currentNode, err := []string{}, (*drive.File)(nil), (*drive.File)(nil), error(nil) + for { + if currentNode, err = gdrive.driveService.Files.Get(nodeID).Fields("*").Do(); err != nil { + break + } + + // Loop to check if is shortcut + for limit := 200_000; limit > 0 && currentNode.MimeType == GoogleDriveMimeSyslink; limit-- { + if currentNode, err = gdrive.driveService.Files.Get(currentNode.ShortcutDetails.TargetId).Fields("*").Do(); err != nil { + break + } + } + + parents := len(currentNode.Parents) + if parents == 0 { + break // Stop count + } else if parents > 1 { + parentsNode, node := []*drive.File{}, (*drive.File)(nil) + for _, parentID := range currentNode.Parents { + if node, err = gdrive.driveService.Files.Get(parentID).Fields("*").Do(); err != nil { + break + } + parentsNode = append(parentsNode, node) + } + slices.SortFunc(parentsNode, func(i, j *drive.File) int { + ia, _ := time.Parse(time.RFC3339, i.CreatedTime) + ja, _ := time.Parse(time.RFC3339, j.CreatedTime) + return ia.Compare(ja) + }) + currentNode = parentsNode[0] + } + + if currentNode.Parents[0] == gdrive.rootDrive.Id { + break // Break loop + } + nodeID = currentNode.Parents[0] // set new nodeID + pathNodes = append(pathNodes, currentNode.Name) // Append name to path + if fistNode == nil { + fistNode = currentNode + } + } + + slices.Reverse(pathNodes) + nodePath := path.Join(pathNodes...) + if err == nil { + gdrive.cachePut(nodePath, fistNode) // Save path to cache + } + + return nodePath, err +} + // Get *drive.File if exist func (gdrive *Gdrive) getNode(path string) (*drive.File, error) { var current *drive.File @@ -233,30 +305,42 @@ func (gdrive *Gdrive) getNode(path string) (*drive.File, error) { return current, nil } -// Save file in path, if folder not exists create -func (gdrive *Gdrive) Save(path string, r io.Reader) (int64, error) { - n := gdrive.pathSplit(path) - if stat, err := gdrive.Stat(path); err == nil { - res, err := gdrive.driveService.Files.Update(stat.(*Stat).File.Id, nil).Media(r).Do() - if err != nil { - return 0, err - } - gdrive.cachePut(n[len(n)-1].Path, res) - return res.Size, nil +// Resolve node path and return New Gdrive struct +func (gdrive *Gdrive) Sub(dir string) (fs.FS, error) { + node, err := gdrive.resolveNode(gdrive.rootDrive.Id, dir) + if err != nil { + return nil, err } - rootSolver := gdrive.rootDrive - if gdrive.checkMkdir(path) { - var err error - if rootSolver, err = gdrive.MkdirAll(n[len(n)-2].Path); err != nil { - return 0, err - } - } - - var err error - if rootSolver, err = gdrive.driveService.Files.Create(&drive.File{MimeType: "application/octet-stream", Name: n[len(n)-1].Name, Parents: []string{rootSolver.Id}}).Fields("*").Media(r).Do(); err != nil { - return 0, err - } - gdrive.cachePut(n[len(n)-1].Path, rootSolver) - return rootSolver.Size, nil + // Return New gdrive struct + return &Gdrive{ + cache: gdrive.cache, + driveService: gdrive.driveService, + GoogleConfig: gdrive.GoogleConfig, + GoogleToken: gdrive.GoogleToken, + rootDrive: node, + }, nil +} + +// Get file stream, if error check if is http2 error to make new request +func (gdrive *Gdrive) getRequest(node *drive.FilesGetCall) (*http.Response, error) { + node.AcknowledgeAbuse(true) + res, err := node.Download() + for i := 0; i < 10 && err != nil; i++ { + if res != nil && res.StatusCode == http.StatusTooManyRequests { + <-time.After(time.Minute) // Wait minutes to reset www.google.com/sorry/index + res, err = node.Download() + continue + } + + if urlError, ok := err.(*url.Error); ok { + if _, ok := urlError.Err.(http2.GoAwayError); ok || reflect.TypeOf(urlError.Err).String() == "http.http2GoAwayError" { + <-time.After(time.Microsecond * 2) // Wait seconds to retry download, to google server close connection + res, err = node.Download() + continue + } + } + break + } + return res, err } diff --git a/go.mod b/go.mod index 2b8fbab..695f200 100644 --- a/go.mod +++ b/go.mod @@ -3,15 +3,18 @@ module sirherobrine23.com.br/Sirherobrine23/drivefs go 1.24 require ( + github.com/valkey-io/valkey-go v1.0.56 golang.org/x/net v0.37.0 golang.org/x/oauth2 v0.28.0 google.golang.org/api v0.225.0 + modernc.org/sqlite v1.36.1 ) require ( cloud.google.com/go/auth v0.15.0 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.7 // indirect cloud.google.com/go/compute/metadata v0.6.0 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect @@ -19,15 +22,22 @@ require ( github.com/google/uuid v1.6.0 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.5 // indirect github.com/googleapis/gax-go/v2 v2.14.1 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/ncruces/go-strftime v0.1.9 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect go.opentelemetry.io/otel v1.35.0 // indirect go.opentelemetry.io/otel/metric v1.35.0 // indirect go.opentelemetry.io/otel/trace v1.35.0 // indirect golang.org/x/crypto v0.36.0 // indirect + golang.org/x/exp v0.0.0-20230315142452-642cacee5cc0 // indirect golang.org/x/sys v0.31.0 // indirect golang.org/x/text v0.23.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250311190419-81fb87f6b8bf // indirect google.golang.org/grpc v1.71.0 // indirect google.golang.org/protobuf v1.36.5 // indirect + modernc.org/libc v1.61.13 // indirect + modernc.org/mathutil v1.7.1 // indirect + modernc.org/memory v1.8.2 // indirect ) diff --git a/go.sum b/go.sum index 47c13f6..3f95539 100644 --- a/go.sum +++ b/go.sum @@ -6,6 +6,8 @@ cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4 cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -17,6 +19,8 @@ github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo= +github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw= github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= @@ -25,50 +29,57 @@ github.com/googleapis/enterprise-certificate-proxy v0.3.5 h1:VgzTY2jogw3xt39CusE github.com/googleapis/enterprise-certificate-proxy v0.3.5/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= github.com/googleapis/gax-go/v2 v2.14.1 h1:hb0FFeiPaQskmvakKu5EbCbpntQn48jyHuvrkurSS/Q= github.com/googleapis/gax-go/v2 v2.14.1/go.mod h1:Hb/NubMaVM88SrNkvl8X/o8XWwDJEPqouaLeN2IUxoA= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= +github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8= +github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/valkey-io/valkey-go v1.0.56 h1:7qp/9dqqPbYEEKeFZCnpX6nzM5XzO2MPp0iKh9+c9Wg= +github.com/valkey-io/valkey-go v1.0.56/go.mod h1:sxpCChk8i3oTG+A/lUi9Lj8C/7WI+yhnQCvDJlPVKNM= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 h1:CV7UdSGJt/Ao6Gp4CXckLxVRRsRgDHoI8XjbL3PDl8s= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0/go.mod h1:FRmFuRJfag1IZ2dPkHnEoSFVgTVPUd2qf5Vi69hLb8I= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ= -go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY= -go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI= go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= -go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ= -go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE= go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= -go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A= -go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU= -go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk= -go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w= -go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k= -go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE= +go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY= +go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg= +go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o= +go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w= go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= +golang.org/x/exp v0.0.0-20230315142452-642cacee5cc0 h1:pVgRXcIictcr+lBQIFeiwuwtDIs4eL21OuM9nyAADmo= +golang.org/x/exp v0.0.0-20230315142452-642cacee5cc0/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= +golang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8= +golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= golang.org/x/oauth2 v0.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc= golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU= +golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ= google.golang.org/api v0.225.0 h1:+4/IVqBQm0MV5S+JW3kdEGC1WtOmM2mXN1LKH1LdNlw= google.golang.org/api v0.225.0/go.mod h1:WP/0Xm4LVvMOCldfvOISnWquSRWbG2kArDZcg+W2DbY= google.golang.org/genproto/googleapis/api v0.0.0-20250106144421-5f5ef82da422 h1:GVIKPyP/kLIyVOgOnTwFOrvQaQUzOzGMCxgFUOEmm24= google.golang.org/genproto/googleapis/api v0.0.0-20250106144421-5f5ef82da422/go.mod h1:b6h1vNKhxaSoEI+5jc3PJUCustfli/mRab7295pY7rw= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb h1:TLPQVbx1GJ8VKZxz52VAxl1EBgKXXbTiU9Fc5fZeLn4= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I= google.golang.org/genproto/googleapis/rpc v0.0.0-20250311190419-81fb87f6b8bf h1:dHDlF3CWxQkefK9IJx+O8ldY0gLygvrlYRBNbPqDWuY= google.golang.org/genproto/googleapis/rpc v0.0.0-20250311190419-81fb87f6b8bf/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I= google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg= @@ -77,3 +88,27 @@ google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwl google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +modernc.org/cc/v4 v4.24.4 h1:TFkx1s6dCkQpd6dKurBNmpo+G8Zl4Sq/ztJ+2+DEsh0= +modernc.org/cc/v4 v4.24.4/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= +modernc.org/ccgo/v4 v4.23.16 h1:Z2N+kk38b7SfySC1ZkpGLN2vthNJP1+ZzGZIlH7uBxo= +modernc.org/ccgo/v4 v4.23.16/go.mod h1:nNma8goMTY7aQZQNTyN9AIoJfxav4nvTnvKThAeMDdo= +modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE= +modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ= +modernc.org/gc/v2 v2.6.3 h1:aJVhcqAte49LF+mGveZ5KPlsp4tdGdAOT4sipJXADjw= +modernc.org/gc/v2 v2.6.3/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= +modernc.org/libc v1.61.13 h1:3LRd6ZO1ezsFiX1y+bHd1ipyEHIJKvuprv0sLTBwLW8= +modernc.org/libc v1.61.13/go.mod h1:8F/uJWL/3nNil0Lgt1Dpz+GgkApWh04N3el3hxJcA6E= +modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= +modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= +modernc.org/memory v1.8.2 h1:cL9L4bcoAObu4NkxOlKWBWtNHIsnnACGF/TbqQ6sbcI= +modernc.org/memory v1.8.2/go.mod h1:ZbjSvMO5NQ1A2i3bWeDiVMxIorXwdClKE/0SZ+BMotU= +modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= +modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= +modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= +modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= +modernc.org/sqlite v1.36.1 h1:bDa8BJUH4lg6EGkLbahKe/8QqoF8p9gArSc6fTqYhyQ= +modernc.org/sqlite v1.36.1/go.mod h1:7MPwH7Z6bREicF9ZVUR78P1IKuxfZ8mRIDHD0iD+8TU= +modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= +modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= +modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= diff --git a/node.go b/node.go deleted file mode 100644 index 0027fd0..0000000 --- a/node.go +++ /dev/null @@ -1,203 +0,0 @@ -package drivefs - -import ( - "errors" - "fmt" - "io" - "io/fs" - "net/http" - "net/url" - "reflect" - "time" - - "golang.org/x/net/http2" - "google.golang.org/api/drive/v3" -) - -var _ fs.File = &Open{} - -type Open struct { - node *drive.File - client *Gdrive - nodeRes *http.Response - offset int64 -} - -func (open *Open) Stat() (fs.FileInfo, error) { return Stat{open.node}, nil } - -func (open *Open) Close() error { - if open.nodeRes == nil || open.nodeRes.Body == nil { - return nil - } - err := open.nodeRes.Body.Close() - open.nodeRes = nil - open.offset = 0 - return err -} - -func (open *Open) Read(p []byte) (int, error) { - if open.nodeRes == nil || open.nodeRes.Body == nil { - node := open.client.driveService.Files.Get(open.node.Id).AcknowledgeAbuse(true) - - // Set Range from offset - if open.offset > 0 && open.node.Size <= open.offset { - node.Header().Set("Range", fmt.Sprintf("bytes=%d-%d", open.offset, open.node.Size-1)) - } - - // Start open request - var err error - if open.nodeRes, err = open.client.getRequest(node); err != nil { - return 0, err - } - } - - n, err := open.nodeRes.Body.Read(p) - if open.offset += int64(n); err != nil && err != io.EOF { - return n, err - } - return n, err -} - -func (open *Open) Seek(offset int64, whence int) (int64, error) { - if offset < 0 { - return 0, errors.New("Seek: invalid offset") - } else if open.nodeRes == nil || open.nodeRes.Body == nil { - return 0, io.EOF - } - - switch whence { - case io.SeekStart: - if offset > open.node.Size { - return 0, io.EOF - } - open.Close() - node := open.client.driveService.Files.Get(open.node.Id).AcknowledgeAbuse(true) - node.Header().Set("Range", fmt.Sprintf("bytes=%d-%d", offset, open.node.Size-1)) - var err error - if open.nodeRes, err = open.client.getRequest(node); err != nil { - return 0, err - } - open.offset = offset - case io.SeekCurrent: - newOffset := open.offset + offset - if newOffset < 0 || newOffset > open.node.Size { - return 0, io.EOF - } else if _, err := io.CopyN(io.Discard, open, offset); err != nil { - return 0, err - } - open.offset = newOffset - case io.SeekEnd: - newOffset := open.node.Size - offset - if newOffset < 0 { - return 0, io.EOF - } - open.Close() - node := open.client.driveService.Files.Get(open.node.Id).AcknowledgeAbuse(true) - node.Header().Set("Range", fmt.Sprintf("bytes=%d-%d", newOffset, open.node.Size-1)) - var err error - if open.nodeRes, err = open.client.getRequest(node); err != nil { - return 0, err - } - open.offset = newOffset - default: - return 0, fs.ErrInvalid - } - - return open.offset, nil -} - -// Get file stream, if error check if is http2 error to make new request -func (gdrive *Gdrive) getRequest(node *drive.FilesGetCall) (*http.Response, error) { - res, err := node.Download() - for i := 0; i < 10 && err != nil; i++ { - if urlError, ok := err.(*url.Error); ok { - if _, ok := urlError.Err.(http2.GoAwayError); ok || reflect.TypeOf(urlError.Err).String() == "http.http2GoAwayError" { - <-time.After(time.Microsecond * 2) // Wait seconds to retry download, to google server close connection - res, err = node.Download() - continue - } else if res != nil && res.StatusCode == 429 { - <-time.After(time.Minute) // Wait minutes to reset www.google.com/sorry/index - res, err = node.Download() - continue - } - } - break - } - return res, err -} - -// resolve path and return File stream -func (gdrive *Gdrive) Open(path string) (fs.File, error) { - fileNode, err := gdrive.getNode(path) - if err != nil { - return nil, err - } - boot, err := gdrive.getRequest(gdrive.driveService.Files.Get(fileNode.Id).AcknowledgeAbuse(true)) - if err != nil { - return nil, err - } - return &Open{fileNode, gdrive, boot, 0}, nil -} - -func (gdrive Gdrive) ReadFile(name string) ([]byte, error) { - file, err := gdrive.Open(name) - if err != nil { - return nil, err - } - defer file.Close() - return io.ReadAll(file) -} - -// Create recursive directory if not exists -func (gdrive *Gdrive) MkdirAll(path string) (*drive.File, error) { - var current *drive.File - if current = gdrive.cacheGet(gdrive.fixPath(path)); current != nil { - return current, nil - } - - current = gdrive.rootDrive // root - nodes := gdrive.pathSplit(path) // split node - for nodeIndex, currentNode := range nodes { - previus := current // storage previus Node - if current = gdrive.cacheGet(currentNode.Path); current != nil { - continue // continue to next node - } - - var err error - // Check if ared exist in folder - if current, err = gdrive.resolveNode(previus.Id, currentNode.Name); err != nil { - if err != fs.ErrNotExist { - return nil, err // return drive error - } - - // Base to create folder - var folderCreate drive.File - folderCreate.MimeType = GoogleDriveMimeFolder // folder mime - folderCreate.Parents = []string{previus.Id} // previus to folder to create - - // Create recursive folder - for _, currentNode = range nodes[nodeIndex:] { - folderCreate.Name = currentNode.Name // folder name - if current, err = gdrive.driveService.Files.Create(&folderCreate).Fields("*").Do(); err != nil { - return nil, err - } - gdrive.cachePut(currentNode.Path, current) - folderCreate.Parents[0] = current.Id // Set new root - } - - // return new folder - return current, nil - } - gdrive.cachePut(currentNode.Path, current) - } - return current, nil -} - -func (gdrive *Gdrive) Delete(path string) error { - fileNode, err := gdrive.getNode(path) - if err != nil { - return err - } - gdrive.cacheDelete(path) - return gdrive.driveService.Files.Delete(fileNode.Id).Do() -} diff --git a/ro.go b/ro.go new file mode 100644 index 0000000..5daed16 --- /dev/null +++ b/ro.go @@ -0,0 +1,112 @@ +package drivefs + +import ( + "io" + "io/fs" + + "google.golang.org/api/drive/v3" +) + +func (gdrive *Gdrive) ReadLink(name string) (string, error) { + fileNode, err := gdrive.getNode(name) + if err != nil { + return "", err + } + + // Loop to check if is shortcut + for limit := 200_000; limit > 0 && fileNode.MimeType == GoogleDriveMimeSyslink; limit-- { + if fileNode, err = gdrive.driveService.Files.Get(fileNode.ShortcutDetails.TargetId).Fields("*").Do(); err != nil { + return "", err + } + } + + return gdrive.forwardPathResove(fileNode.Id) +} + +func (gdrive *Gdrive) Lstat(name string) (fs.FileInfo, error) { + fileNode, err := gdrive.getNode(name) + if err != nil { + return nil, err + } + return &Stat{fileNode}, nil +} + +// Resolve path and return File or Folder Stat +func (gdrive *Gdrive) Stat(path string) (fs.FileInfo, error) { + fileNode, err := gdrive.getNode(path) + if err != nil { + return nil, err + } + + // Loop to check if is shortcut + for limit := 200_000; limit > 0 && fileNode.MimeType == GoogleDriveMimeSyslink; limit-- { + if fileNode, err = gdrive.driveService.Files.Get(fileNode.ShortcutDetails.TargetId).Do(); err != nil { + return nil, err + } + } + + return &Stat{fileNode}, nil +} + +// List files and folder in Directory +func (gdrive *Gdrive) ReadDir(name string) ([]fs.DirEntry, error) { + current, err := (*drive.File)(nil), error(nil) + if current, err = gdrive.getNode(name); err != nil { + return nil, err + } + + nodes, err := gdrive.listNodes(current.Id) + if err != nil { + return nil, err + } + + entrysSlice := []fs.DirEntry{} + for index := range nodes { + entrysSlice = append(entrysSlice, fs.FileInfoToDirEntry(&Stat{nodes[index]})) + } + + return entrysSlice, nil +} + +// resolve path and return File stream +func (gdrive *Gdrive) Open(name string) (fs.File, error) { + node, err := gdrive.getNode(name) + if err != nil { + return nil, err + } + + if node.MimeType == GoogleDriveMimeFolder { + nodes, err := gdrive.listNodes(node.Id) + if err != nil { + return nil, err + } + + nodeFiles := []fs.DirEntry{} + for _, node := range nodes { + nodeFiles = append(nodeFiles, fs.FileInfoToDirEntry(&Stat{File: node})) + } + return &GdriveNode{filename: name, gClient: gdrive, node: node, nodeFiles: nodeFiles, filesOffset: 0, direction: DirectionWrite}, nil + } + + boot, err := gdrive.getRequest(gdrive.driveService.Files.Get(node.Id)) + if err != nil { + return nil, err + } + + return &GdriveNode{ + filename: name, + gClient: gdrive, + node: node, + sReadRes: boot, + direction: DirectionReader, + }, nil +} + +func (gdrive Gdrive) ReadFile(name string) ([]byte, error) { + file, err := gdrive.Open(name) + if err != nil { + return nil, err + } + defer file.Close() + return io.ReadAll(file) +} diff --git a/rw.go b/rw.go new file mode 100644 index 0000000..f2d46e3 --- /dev/null +++ b/rw.go @@ -0,0 +1,126 @@ +package drivefs + +import ( + "io" + "io/fs" + + "google.golang.org/api/drive/v3" +) + +// Delete file from google drive +// +// Deprecated: use [sirherobrine23.com.br/Sirherobrine23/drivefs.Gdrive.Remove] +func (gdrive *Gdrive) Delete(name string) error { + return gdrive.Remove(name) +} + +// Link to [sirherobrine23.com.br/Sirherobrine23/drivefs.Gdrive.Remove] +func (gdrive *Gdrive) RemoveAll(name string) error { + return gdrive.Remove(name) +} + +// Delete file from google drive if is folder delete recursive +func (gdrive *Gdrive) Remove(name string) error { + fileNode, err := gdrive.getNode(name) + if err != nil { + return err + } + gdrive.cacheDelete(name) + return gdrive.driveService.Files.Delete(fileNode.Id).Do() +} + +// Create recursive directory if not exists +func (gdrive *Gdrive) mkdirAllNodes(path string) (*drive.File, error) { + var current *drive.File + if current = gdrive.cacheGet(gdrive.fixPath(path)); current != nil { + return current, nil + } + + current = gdrive.rootDrive // root + nodes := gdrive.pathSplit(path) // split node + for nodeIndex, currentNode := range nodes { + previus := current // storage previus Node + if current = gdrive.cacheGet(currentNode.Path); current != nil { + continue // continue to next node + } + + var err error + // Check if ared exist in folder + if current, err = gdrive.resolveNode(previus.Id, currentNode.Name); err != nil { + if err != fs.ErrNotExist { + return nil, err // return drive error + } + + // Base to create folder + var folderCreate drive.File + folderCreate.MimeType = GoogleDriveMimeFolder // folder mime + folderCreate.Parents = []string{previus.Id} // previus to folder to create + + // Create recursive folder + for _, currentNode = range nodes[nodeIndex:] { + folderCreate.Name = currentNode.Name // folder name + if current, err = gdrive.driveService.Files.Create(&folderCreate).Fields("*").Do(); err != nil { + return nil, err + } + gdrive.cachePut(currentNode.Path, current) + folderCreate.Parents[0] = current.Id // Set new root + } + + // return new folder + return current, nil + } + gdrive.cachePut(currentNode.Path, current) + } + return current, nil +} + +func (gdrive *Gdrive) MkdirAll(name string) (err error) { + _, err = gdrive.mkdirAllNodes(name) + return +} + +// Save file in path, if folder not exists create +// +// Deprecated: use [sirherobrine23.com.br/Sirherobrine23/drivefs.Create] +func (gdrive *Gdrive) Save(name string, r io.Reader) (int64, error) { + f, err := gdrive.Create(name) + if err != nil { + return 0, err + } + defer f.Close() + return io.Copy(f, r) +} + +// Create file if not exists +func (gdrive *Gdrive) Create(name string) (file File, err error) { + node := (*drive.File)(nil) + if stat, err2 := gdrive.Stat(name); err2 == nil { + node = stat.(*Stat).File + } else if gdrive.checkMkdir(name) { + pathNodes := gdrive.pathSplit(name) + if node, err = gdrive.mkdirAllNodes(pathNodes[len(pathNodes)-2].Path); err != nil { + return + } + file = &GdriveNode{filename: name, gClient: gdrive, nodeRoot: node, direction: DirectionWrite} + return + } + + if node == nil { + file = &GdriveNode{filename: name, gClient: gdrive, node: nil, direction: DirectionWrite} + return + } else if node.MimeType == GoogleDriveMimeFolder { + nodes, err := gdrive.listNodes(node.Id) + if err != nil { + return nil, err + } + + nodeFiles := []fs.DirEntry{} + for _, node := range nodes { + nodeFiles = append(nodeFiles, fs.FileInfoToDirEntry(&Stat{File: node})) + } + return &GdriveNode{filename: name, gClient: gdrive, node: node, nodeFiles: nodeFiles, filesOffset: 0, direction: DirectionWrite}, nil + } + + file = &GdriveNode{filename: name, gClient: gdrive, node: node, direction: DirectionWrite} + return +} diff --git a/stat.go b/stat.go deleted file mode 100644 index 32da191..0000000 --- a/stat.go +++ /dev/null @@ -1,69 +0,0 @@ -package drivefs - -import ( - "io/fs" - "path/filepath" - "time" - - "google.golang.org/api/drive/v3" -) - -type Stat struct { - *drive.File -} - -func (node Stat) Name() string { return filepath.Clean(node.File.Name) } -func (node Stat) Size() int64 { return node.File.Size } -func (node Stat) IsDir() bool { return node.File.MimeType == GoogleDriveMimeFolder } - -func (node Stat) Sys() any { return node.File } - -func (node Stat) ModTime() time.Time { - t := time.Date(0, 0, 0, 0, 0, 0, 0, time.UTC) - for _, fileTime := range []string{node.File.ModifiedTime, node.File.CreatedTime} { - if fileTime == "" { - continue - } - t.UnmarshalText([]byte(fileTime)) - break - } - return t -} - -func (node Stat) Mode() fs.FileMode { - if node.File.MimeType == GoogleDriveMimeFolder { - return fs.ModeDir | fs.ModePerm - } else if node.File.MimeType == GoogleDriveMimeSyslink { - return fs.ModeSymlink | fs.ModePerm - } - return fs.ModePerm -} - -// Resolve path and return File or Folder Stat -func (gdrive *Gdrive) Stat(path string) (fs.FileInfo, error) { - fileNode, err := gdrive.getNode(path) - if err != nil { - return nil, err - } - return &Stat{fileNode}, nil -} - -// List files and folder in Directory -func (gdrive *Gdrive) ReadDir(name string) ([]fs.DirEntry, error) { - current, err := (*drive.File)(nil), error(nil) - if current, err = gdrive.getNode(name); err != nil { - return nil, err - } - - nodes, err := gdrive.listNodes(current.Id) - if err != nil { - return nil, err - } - - entrysSlice := []fs.DirEntry{} - for index := range nodes { - entrysSlice = append(entrysSlice, fs.FileInfoToDirEntry(&Stat{nodes[index]})) - } - - return entrysSlice, nil -}