Add Readlink, Lstat and refactor code #1

Merged
Sirherobrine23 merged 1 commits from add_Readlink-and-Lstat into main 2025-03-17 03:59:01 +00:00
16 changed files with 1427 additions and 463 deletions

7
.gitignore vendored
View File

@ -25,5 +25,8 @@ go.work.sum
# env file
.env
# Google config
config.json
# Database
*.db
# Google client
/*.json

143
README.md
View File

@ -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:<ID>) 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, "<html><body>Code: %q</body></html>", 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)
}
}
```

113
cache/cache.go vendored
View File

@ -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
}

156
cache/db.go vendored Normal file
View File

@ -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)
}
}
}

57
cache/db_test.go vendored Normal file
View File

@ -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
}
}

70
cache/memory.go vendored Normal file
View File

@ -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
}

44
cache/valkey.go vendored Normal file
View File

@ -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)
}

143
example/main.go Normal file
View File

@ -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:<ID>) 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, "<html><body>Code: %q</body></html>", 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)
}
}

356
file.go Normal file
View File

@ -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
}

218
gdrive.go
View File

@ -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:<ID>) 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:<ID>) 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
}

10
go.mod
View File

@ -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
)

63
go.sum
View File

@ -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=

203
node.go
View File

@ -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()
}

112
ro.go Normal file
View File

@ -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)
}

126
rw.go Normal file
View File

@ -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
}

69
stat.go
View File

@ -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
}