Add Readlink, Lstat and refactor code #1
7
.gitignore
vendored
7
.gitignore
vendored
@ -25,5 +25,8 @@ go.work.sum
|
|||||||
# env file
|
# env file
|
||||||
.env
|
.env
|
||||||
|
|
||||||
# Google config
|
# Database
|
||||||
config.json
|
*.db
|
||||||
|
|
||||||
|
# Google client
|
||||||
|
/*.json
|
147
README.md
147
README.md
@ -22,87 +22,134 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
|
|
||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
"google.golang.org/api/drive/v3"
|
"google.golang.org/api/drive/v2"
|
||||||
"sirherobrine23.org/Sirherobrine23/drivefs"
|
"sirherobrine23.com.br/Sirherobrine23/drivefs"
|
||||||
)
|
)
|
||||||
|
|
||||||
var configPath string
|
var (
|
||||||
var serverPort uint
|
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() {
|
func main() {
|
||||||
flag.StringVar(&configPath, "config", "./config.json", "Config file path")
|
|
||||||
flag.UintVar(&serverPort, "port", 8081, "server to listen")
|
|
||||||
flag.Parse()
|
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)
|
fileConfig, err := os.ReadFile(*configPath)
|
||||||
file, err := os.Open(configPath)
|
if err == nil {
|
||||||
if err != nil {
|
if err = json.Unmarshal(fileConfig, &gdriveConfig); err != nil {
|
||||||
panic(err)
|
fmt.Fprintf(os.Stderr, "Cannot unmarshall config: %s\n", err)
|
||||||
}
|
os.Exit(1)
|
||||||
defer file.Close()
|
return
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
} else if os.IsNotExist(err) {
|
||||||
} else {
|
} else {
|
||||||
|
fmt.Fprintf(os.Stderr, "Cannot open %q: %s\n", *configPath, err)
|
||||||
|
os.Exit(1)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if *setupAuth {
|
||||||
ln, err := net.Listen("tcp", ":0")
|
ln, err := net.Listen("tcp", ":0")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
P, _ := netip.ParseAddrPort(ln.Addr().String())
|
P, _ := netip.ParseAddrPort(ln.Addr().String())
|
||||||
ln.Close()
|
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,
|
||||||
authURL := config.AuthCodeURL("state-token", oauth2.AccessTypeOffline)
|
ClientSecret: gdriveConfig.Secret,
|
||||||
fmt.Printf("Go to the following link in your browser then type the authorization code: \n%v\n", authURL)
|
RedirectURL: fmt.Sprintf("http://localhost:%d/callback", P.Port()),
|
||||||
|
Scopes: []string{drive.DriveScope, drive.DriveFileScope},
|
||||||
var server *http.Server
|
Endpoint: oauth2.Endpoint{
|
||||||
var code string
|
AuthURL: gdriveConfig.AuthURI,
|
||||||
mux := http.NewServeMux()
|
TokenURL: gdriveConfig.TokenURI,
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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}
|
server = &http.Server{Addr: P.String(), Handler: mux}
|
||||||
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||||
panic(err)
|
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 {
|
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)
|
panic(err)
|
||||||
}
|
} else if err = os.WriteFile(*configPath, data, 0666); err != nil {
|
||||||
|
|
||||||
at := json.NewEncoder(file)
|
|
||||||
at.SetIndent("", " ")
|
|
||||||
if err := at.Encode(ggdrive); err != nil {
|
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("server listening on :%d\n", serverPort)
|
gdrive, err := drivefs.NewGoogleDrive(gdriveConfig)
|
||||||
if err := http.ListenAndServe(fmt.Sprintf(":%d", serverPort), http.FileServerFS(ggdrive)); err != nil {
|
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())
|
fmt.Fprintln(os.Stderr, err.Error())
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
```
|
```
|
103
cache/cache.go
vendored
103
cache/cache.go
vendored
@ -1,72 +1,65 @@
|
|||||||
package cache
|
package cache
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"sync"
|
"encoding"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"iter"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type cacheInfo[D any] struct {
|
var (
|
||||||
TimeValid time.Time // Valid time
|
ErrNotExist error = errors.New("key not exists")
|
||||||
Data D // Data
|
)
|
||||||
|
|
||||||
|
// 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 {
|
func ToString(v any) (string, error) {
|
||||||
l map[string]*cacheInfo[T]
|
switch v := v.(type) {
|
||||||
|
case encoding.TextMarshaler:
|
||||||
rw sync.Mutex
|
data, err := v.MarshalText()
|
||||||
}
|
if err != nil {
|
||||||
|
return "", err
|
||||||
// Get value from Key
|
|
||||||
func (w *LocalCache[T]) Get(Key string) (T, bool) {
|
|
||||||
if len(w.l) == 0 {
|
|
||||||
return *new(T), false
|
|
||||||
}
|
}
|
||||||
|
return string(data), nil
|
||||||
w.rw.Lock()
|
case encoding.BinaryMarshaler:
|
||||||
defer w.rw.Unlock()
|
data, err := v.MarshalBinary()
|
||||||
data, ok := w.l[Key]
|
if err != nil {
|
||||||
if ok {
|
return "", err
|
||||||
if data.TimeValid.Unix() >= time.Now().Unix() {
|
|
||||||
delete(w.l, Key)
|
|
||||||
return *new(T), false
|
|
||||||
}
|
}
|
||||||
return data.Data, true
|
return string(data), nil
|
||||||
|
case json.Marshaler:
|
||||||
|
data, err := v.MarshalJSON()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
}
|
}
|
||||||
return *new(T), false
|
return string(data), nil
|
||||||
}
|
default:
|
||||||
|
data, err := json.Marshal(v)
|
||||||
// Set value to cache struct
|
if err != nil {
|
||||||
func (w *LocalCache[T]) Set(ValidAt time.Time, Key string, Value T) {
|
return "", err
|
||||||
w.rw.Lock()
|
|
||||||
defer w.rw.Unlock()
|
|
||||||
if len(w.l) == 0 {
|
|
||||||
w.l = make(map[string]*cacheInfo[T])
|
|
||||||
}
|
}
|
||||||
|
return string(data), nil
|
||||||
w.l[Key] = &cacheInfo[T]{
|
|
||||||
TimeValid: ValidAt,
|
|
||||||
Data: Value,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete key if exists
|
func FromString[T any](value string) (target T, err error) {
|
||||||
func (w *LocalCache[T]) Delete(Key string) {
|
switch v := any(target).(type) {
|
||||||
w.rw.Lock()
|
case encoding.TextUnmarshaler:
|
||||||
defer w.rw.Unlock()
|
err = v.UnmarshalText([]byte(value))
|
||||||
delete(w.l, Key)
|
case encoding.BinaryUnmarshaler:
|
||||||
}
|
err = v.UnmarshalBinary([]byte(value))
|
||||||
|
case json.Unmarshaler:
|
||||||
// Remove expired Cache
|
err = v.UnmarshalJSON([]byte(value))
|
||||||
func (w *LocalCache[T]) Flush() int {
|
default:
|
||||||
w.rw.Lock()
|
err = json.Unmarshal([]byte(value), &value)
|
||||||
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
|
||||||
return flushed
|
|
||||||
}
|
}
|
||||||
|
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
|
||||||
|
}
|
214
gdrive.go
214
gdrive.go
@ -3,12 +3,17 @@ package drivefs
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"io/fs"
|
"io/fs"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"reflect"
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/net/http2"
|
||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
"google.golang.org/api/drive/v3"
|
"google.golang.org/api/drive/v3"
|
||||||
"google.golang.org/api/option"
|
"google.golang.org/api/option"
|
||||||
@ -28,6 +33,22 @@ var (
|
|||||||
_ fs.ReadDirFS = &Gdrive{}
|
_ fs.ReadDirFS = &Gdrive{}
|
||||||
_ fs.ReadFileFS = &Gdrive{}
|
_ fs.ReadFileFS = &Gdrive{}
|
||||||
_ fs.SubFS = &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 {
|
type Fs interface {
|
||||||
@ -36,37 +57,42 @@ type Fs interface {
|
|||||||
fs.ReadDirFS
|
fs.ReadDirFS
|
||||||
fs.ReadFileFS
|
fs.ReadFileFS
|
||||||
fs.SubFS
|
fs.SubFS
|
||||||
fs.GlobFS
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Gdrive struct {
|
type Gdrive struct {
|
||||||
GoogleConfig *oauth2.Config // Google client app oauth project
|
GoogleConfig *oauth2.Config `json:"client"` // Google client app oauth project
|
||||||
GoogleToken *oauth2.Token // Authenticated user
|
GoogleToken *oauth2.Token `json:"token"` // Authenticated user
|
||||||
|
|
||||||
driveService *drive.Service // Google drive service
|
driveService *drive.Service // Google drive service
|
||||||
rootDrive *drive.File // Root to find files
|
rootDrive *drive.File // Root to find files
|
||||||
|
cache cache.Cache[*drive.File] // Cache struct
|
||||||
cache *cache.LocalCache[*drive.File]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GoogleOauthConfig represents google oauth token for drive setup
|
// GoogleOauthConfig represents google oauth token for drive setup
|
||||||
type GoogleOauthConfig struct {
|
type GoogleOauthConfig struct {
|
||||||
Client string `json:",omitempty"`
|
Client string `json:"client,omitempty"` // installed.client_id
|
||||||
Secret string `json:",omitempty"`
|
Secret string `json:"secret,omitempty"` // installed.client_secret
|
||||||
Project string `json:",omitempty"`
|
Project string `json:"project,omitempty"` // installed.project_id
|
||||||
AuthURI string `json:",omitempty"`
|
AuthURI string `json:"auth_uri,omitempty"` // installed.auth_uri
|
||||||
TokenURI string `json:",omitempty"`
|
TokenURI string `json:"token_uri,omitempty"` // installed.token_uri
|
||||||
Redirect string `json:",omitempty"`
|
Redirect string `json:"redirect,omitempty"` // installed.redirect_uris[]
|
||||||
AccessToken string `json:",omitempty"`
|
AccessToken string `json:"access_token,omitempty"` // token.access_token
|
||||||
RefreshToken string `json:",omitempty"`
|
RefreshToken string `json:"refresh_token,omitempty"` // token.refresh_token
|
||||||
Expire time.Time `json:",omitempty"`
|
TokenType string `json:"token_type,omitempty"` // token.token_type
|
||||||
TokenType string `json:",omitempty"`
|
Expire time.Time `json:"expire,omitzero"` // token.expiry
|
||||||
RootFolder string `json:",omitempty"` // Google drive folder id (gdrive:<ID>) or path to folder
|
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
|
// 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{
|
gdrive := &Gdrive{
|
||||||
cache: &cache.LocalCache[*drive.File]{},
|
cache: config.Cacher,
|
||||||
GoogleConfig: &oauth2.Config{
|
GoogleConfig: &oauth2.Config{
|
||||||
ClientID: config.Client,
|
ClientID: config.Client,
|
||||||
ClientSecret: config.Secret,
|
ClientSecret: config.Secret,
|
||||||
@ -98,13 +124,13 @@ func NewGoogleDrive(config GoogleOauthConfig) (*Gdrive, error) {
|
|||||||
return nil, fmt.Errorf("cannot get root: %v", err)
|
return nil, fmt.Errorf("cannot get root: %v", err)
|
||||||
}
|
}
|
||||||
n = n[1:]
|
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
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// resolve and create path not exists in new root
|
// resolve and create path not exists in new root
|
||||||
if len(n) >= 1 {
|
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
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -116,15 +142,15 @@ func NewGoogleDrive(config GoogleOauthConfig) (*Gdrive, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (gdrive *Gdrive) cacheDelete(path string) {
|
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) {
|
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 {
|
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 node
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@ -132,6 +158,10 @@ func (gdrive *Gdrive) cacheGet(path string) *drive.File {
|
|||||||
|
|
||||||
// Get Node info and is not trashed/deleted
|
// Get Node info and is not trashed/deleted
|
||||||
func (gdrive *Gdrive) resolveNode(folderID, name string) (*drive.File, error) {
|
func (gdrive *Gdrive) resolveNode(folderID, name string) (*drive.File, error) {
|
||||||
|
if name == "." || name == "/" {
|
||||||
|
return gdrive.rootDrive, nil
|
||||||
|
}
|
||||||
|
|
||||||
name = strings.ReplaceAll(strings.ReplaceAll(name, `\`, `\\`), `'`, `\'`)
|
name = strings.ReplaceAll(strings.ReplaceAll(name, `\`, `\\`), `'`, `\'`)
|
||||||
file, err := gdrive.driveService.Files.List().Fields("*").PageSize(300).Q(fmt.Sprintf(GoogleListQueryWithName, folderID, name)).Do()
|
file, err := gdrive.driveService.Files.List().Fields("*").PageSize(300).Q(fmt.Sprintf(GoogleListQueryWithName, folderID, name)).Do()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -155,7 +185,13 @@ func (gdrive *Gdrive) listNodes(folderID string) ([]*drive.File, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nodes, err
|
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 == "" {
|
if folderGdrive.PageToken(res.NextPageToken); res.NextPageToken == "" {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@ -163,23 +199,6 @@ func (gdrive *Gdrive) listNodes(folderID string) ([]*drive.File, error) {
|
|||||||
return nodes, nil
|
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
|
// Split to nodes
|
||||||
func (*Gdrive) pathSplit(path string) []struct{ Name, Path string } {
|
func (*Gdrive) pathSplit(path string) []struct{ Name, Path string } {
|
||||||
path = strings.Trim(filepath.ToSlash(path), "/")
|
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]
|
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
|
// Get *drive.File if exist
|
||||||
func (gdrive *Gdrive) getNode(path string) (*drive.File, error) {
|
func (gdrive *Gdrive) getNode(path string) (*drive.File, error) {
|
||||||
var current *drive.File
|
var current *drive.File
|
||||||
@ -233,30 +305,42 @@ func (gdrive *Gdrive) getNode(path string) (*drive.File, error) {
|
|||||||
return current, nil
|
return current, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save file in path, if folder not exists create
|
// Resolve node path and return New Gdrive struct
|
||||||
func (gdrive *Gdrive) Save(path string, r io.Reader) (int64, error) {
|
func (gdrive *Gdrive) Sub(dir string) (fs.FS, error) {
|
||||||
n := gdrive.pathSplit(path)
|
node, err := gdrive.resolveNode(gdrive.rootDrive.Id, dir)
|
||||||
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 {
|
if err != nil {
|
||||||
return 0, err
|
return nil, err
|
||||||
}
|
|
||||||
gdrive.cachePut(n[len(n)-1].Path, res)
|
|
||||||
return res.Size, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
rootSolver := gdrive.rootDrive
|
// Return New gdrive struct
|
||||||
if gdrive.checkMkdir(path) {
|
return &Gdrive{
|
||||||
var err error
|
cache: gdrive.cache,
|
||||||
if rootSolver, err = gdrive.MkdirAll(n[len(n)-2].Path); err != nil {
|
driveService: gdrive.driveService,
|
||||||
return 0, err
|
GoogleConfig: gdrive.GoogleConfig,
|
||||||
}
|
GoogleToken: gdrive.GoogleToken,
|
||||||
}
|
rootDrive: node,
|
||||||
|
}, nil
|
||||||
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
|
// Get file stream, if error check if is http2 error to make new request
|
||||||
}
|
func (gdrive *Gdrive) getRequest(node *drive.FilesGetCall) (*http.Response, error) {
|
||||||
gdrive.cachePut(n[len(n)-1].Path, rootSolver)
|
node.AcknowledgeAbuse(true)
|
||||||
return rootSolver.Size, nil
|
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
|
go 1.24
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/valkey-io/valkey-go v1.0.56
|
||||||
golang.org/x/net v0.37.0
|
golang.org/x/net v0.37.0
|
||||||
golang.org/x/oauth2 v0.28.0
|
golang.org/x/oauth2 v0.28.0
|
||||||
google.golang.org/api v0.225.0
|
google.golang.org/api v0.225.0
|
||||||
|
modernc.org/sqlite v1.36.1
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
cloud.google.com/go/auth v0.15.0 // indirect
|
cloud.google.com/go/auth v0.15.0 // indirect
|
||||||
cloud.google.com/go/auth/oauth2adapt v0.2.7 // indirect
|
cloud.google.com/go/auth/oauth2adapt v0.2.7 // indirect
|
||||||
cloud.google.com/go/compute/metadata v0.6.0 // 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/felixge/httpsnoop v1.0.4 // indirect
|
||||||
github.com/go-logr/logr v1.4.2 // indirect
|
github.com/go-logr/logr v1.4.2 // indirect
|
||||||
github.com/go-logr/stdr v1.2.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/google/uuid v1.6.0 // indirect
|
||||||
github.com/googleapis/enterprise-certificate-proxy v0.3.5 // indirect
|
github.com/googleapis/enterprise-certificate-proxy v0.3.5 // indirect
|
||||||
github.com/googleapis/gax-go/v2 v2.14.1 // 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/auto/sdk v1.1.0 // indirect
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.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 v1.35.0 // indirect
|
||||||
go.opentelemetry.io/otel/metric v1.35.0 // indirect
|
go.opentelemetry.io/otel/metric v1.35.0 // indirect
|
||||||
go.opentelemetry.io/otel/trace v1.35.0 // indirect
|
go.opentelemetry.io/otel/trace v1.35.0 // indirect
|
||||||
golang.org/x/crypto v0.36.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/sys v0.31.0 // indirect
|
||||||
golang.org/x/text v0.23.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/genproto/googleapis/rpc v0.0.0-20250311190419-81fb87f6b8bf // indirect
|
||||||
google.golang.org/grpc v1.71.0 // indirect
|
google.golang.org/grpc v1.71.0 // indirect
|
||||||
google.golang.org/protobuf v1.36.5 // 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=
|
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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
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 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
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=
|
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/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 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
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 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
|
||||||
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
|
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
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/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 h1:hb0FFeiPaQskmvakKu5EbCbpntQn48jyHuvrkurSS/Q=
|
||||||
github.com/googleapis/gax-go/v2 v2.14.1/go.mod h1:Hb/NubMaVM88SrNkvl8X/o8XWwDJEPqouaLeN2IUxoA=
|
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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
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 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
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 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
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 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU=
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ=
|
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 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
|
||||||
go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
|
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 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
|
||||||
go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
|
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.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY=
|
||||||
go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU=
|
go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg=
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk=
|
go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o=
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w=
|
go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w=
|
||||||
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/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
|
go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
|
||||||
go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
|
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 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
|
||||||
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
|
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 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
|
||||||
golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
|
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 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc=
|
||||||
golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
|
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 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
|
||||||
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
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 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
|
||||||
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
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 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
|
||||||
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
|
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 h1:+4/IVqBQm0MV5S+JW3kdEGC1WtOmM2mXN1LKH1LdNlw=
|
||||||
google.golang.org/api v0.225.0/go.mod h1:WP/0Xm4LVvMOCldfvOISnWquSRWbG2kArDZcg+W2DbY=
|
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 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/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 h1:dHDlF3CWxQkefK9IJx+O8ldY0gLygvrlYRBNbPqDWuY=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250311190419-81fb87f6b8bf/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I=
|
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=
|
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=
|
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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
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