Add Readlink, Lstat and refactor code #1
7
.gitignore
vendored
7
.gitignore
vendored
@ -25,5 +25,8 @@ go.work.sum
|
||||
# env file
|
||||
.env
|
||||
|
||||
# Google config
|
||||
config.json
|
||||
# Database
|
||||
*.db
|
||||
|
||||
# Google client
|
||||
/*.json
|
143
README.md
143
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:<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
113
cache/cache.go
vendored
@ -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
156
cache/db.go
vendored
Normal 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
57
cache/db_test.go
vendored
Normal 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
70
cache/memory.go
vendored
Normal 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
44
cache/valkey.go
vendored
Normal 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
143
example/main.go
Normal 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
356
file.go
Normal 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
218
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:<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
10
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
|
||||
)
|
||||
|
63
go.sum
63
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=
|
||||
|
203
node.go
203
node.go
@ -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
112
ro.go
Normal 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
126
rw.go
Normal 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
69
stat.go
@ -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
|
||||
}
|
Reference in New Issue
Block a user