diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4df5abc09..677c50546 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -96,13 +96,11 @@ docker run -d --name arangodb \ -e ARANGO_ROOT_PASSWORD=rootpassword \ --pull always \ arangodb:latest -<<<<<<< HEAD docker run -d --name db -p 8091-8096:8091-8096 -p 11210-11211:11210-11211 couchbase -======= docker login container-registry.oracle.com docker pull container-registry.oracle.com/database/free:latest docker run -d --name oracle-free -p 1521:1521 -e ORACLE_PWD=YourPasswordHere container-registry.oracle.com/database/free:latest ->>>>>>> origin +docker run -it --rm -p 4443:4443 -e STORAGE_EMULATOR_HOST=0.0.0.0:4443 fsouza/fake-gcs-server:latest ``` > [!NOTE] diff --git a/docs/advanced-guide/handling-file/page.md b/docs/advanced-guide/handling-file/page.md index fb9649e22..e4edf74ff 100644 --- a/docs/advanced-guide/handling-file/page.md +++ b/docs/advanced-guide/handling-file/page.md @@ -6,9 +6,15 @@ GoFr simplifies the complexity of working with different file stores by offering By default, local file-store is initialized and user can access it from the context. -GoFr also supports FTP/SFTP file-store. Developers can also connect and use their AWS S3 bucket as a file-store. The file-store can be initialized as follows: +GoFr also supports FTP/SFTP file-store. Developers can also connect and use their cloud storage bucket as a file-store. Following cloud storage options are currently supported: + +- **AWS S3** +- **Google Cloud Storage (GCS)** + +The file-store can be initialized as follows: ### FTP file-store + ```go package main @@ -34,6 +40,7 @@ func main() { ``` ### SFTP file-store + ```go package main @@ -60,8 +67,7 @@ func main() { ### AWS S3 Bucket as File-Store To run S3 File-Store locally we can use localstack, -``docker run --rm -it -p 4566:4566 -p 4510-4559:4510-4559 localstack/localstack`` - +`docker run --rm -it -p 4566:4566 -p 4510-4559:4510-4559 localstack/localstack` ```go package main @@ -90,17 +96,68 @@ func main() { app.Run() } ``` -> Note: The current implementation supports handling only one bucket at a time, -> as shown in the example with `gofr-bucket-2`. Bucket switching mid-operation is not supported. + +> Note: The current implementation supports handling only one bucket at a time, +> as shown in the example with `gofr-bucket-2`. Bucket switching mid-operation is not supported. + +### Google Cloud Storage (GCS) Bucket as File-Store + +To run GCS File-Store locally we can use fake-gcs-server: +`docker run -it --rm -p 4443:4443 -e STORAGE_EMULATOR_HOST=0.0.0.0:4443 fsouza/fake-gcs-server:latest` + +```go +package main + +import ( + "gofr.dev/pkg/gofr" + + "gofr.dev/pkg/gofr/datasource/file/gcs" +) + +func main() { + app := gofr.New() + + // Option 1: Using JSON credentials string + app.AddFileStore(gcs.New(&gcs.Config{ + BucketName: "my-bucket", + CredentialsJSON: readFile("gcs-credentials.json"), + ProjectID: "my-project-id", + })) + + // Option 2: Using default credentials from env + // app.AddFileStore(gcs.New(&gcs.Config{ + // BucketName: "my-bucket", + // ProjectID: "my-project-id", + // })) + + app.Run() +} + +// Helper function to read credentials file +func readFile(filename string) []byte { + data, err := os.ReadFile(filename) + if err != nil { + log.Fatalf("Failed to read credentials file: %v", err) + } + return data +} + +``` + +> **Note:** When connecting to the actual GCS service, authentication can be provided via CredentialsJSON or the GOOGLE_APPLICATION_CREDENTIALS environment variable. +> When using fake-gcs-server, authentication is not required. +> Currently supports one bucket per file-store instance. ### Creating Directory To create a single directory + ```go err := ctx.File.Mkdir("my_dir",os.ModePerm) ``` To create subdirectories as well + ```go err := ctx.File.MkdirAll("my_dir/sub_dir", os.ModePerm) ``` @@ -114,27 +171,32 @@ currentDir, err := ctx.File.Getwd() ### Change current Directory To switch to parent directory + ```go currentDir, err := ctx.File.Chdir("..") ``` To switch to another directory in same parent directory + ```go currentDir, err := ctx.File.Chdir("../my_dir2") ``` To switch to a subfolder of the current directory + ```go currentDir, err := ctx.File.Chdir("sub_dir") ``` + > Note: This method attempts to change the directory, but S3's flat structure and fixed bucket -> make this operation inapplicable. +> make this operation inapplicable. Similarly, GCS uses a flat structure where directories are simulated through object prefixes. ### Read a Directory -The ReadDir function reads the specified directory and returns a sorted list of its entries as FileInfo objects. Each FileInfo object provides access to its associated methods, eliminating the need for additional stat calls. +The ReadDir function reads the specified directory and returns a sorted list of its entries as FileInfo objects. Each FileInfo object provides access to its associated methods, eliminating the need for additional stat calls. If an error occurs during the read operation, ReadDir returns the successfully read entries up to the point of the error along with the error itself. Passing "." as the directory argument returns the entries for the current directory. + ```go entries, err := ctx.File.ReadDir("../testdir") @@ -143,12 +205,13 @@ for _, entry := range entries { if entry.IsDir() { entryType = "Dir" - } + } fmt.Printf("%v: %v Size: %v Last Modified Time : %v\n", entryType, entry.Name(), entry.Size(), entry.ModTime()) } ``` -> Note: In S3, directories are represented as prefixes of file keys. This method retrieves file + +> Note: In S3 and GCS, directories are represented as prefixes of file keys/object names. This method retrieves file > entries only from the immediate level within the specified directory. ### Creating and Save a File with Content @@ -163,6 +226,7 @@ _, _ = file.Write([]byte("Hello World!")) ``` ### Reading file as CSV/JSON/TEXT + GoFr support reading CSV/JSON/TEXT files line by line. ```go @@ -170,7 +234,7 @@ reader, err := file.ReadAll() for reader.Next() { var b string - + // For reading CSV/TEXT files user need to pass pointer to string to SCAN. // In case of JSON user should pass structs with JSON tags as defined in encoding/json. err = reader.Scan(&b) @@ -179,10 +243,12 @@ for reader.Next() { } ``` - ### Opening and Reading Content from a File + To open a file with default settings, use the `Open` command, which provides read and seek permissions only. For write permissions, use `OpenFile` with the appropriate file modes. + > Note: In FTP, file permissions are not differentiated; both `Open` and `OpenFile` allow all file operations regardless of specified permissions. + ```go csvFile, _ := ctx.File.Open("my_file.csv") @@ -205,6 +271,7 @@ if err != nil { ### Getting Information of the file/directory Stat retrieves details of a file or directory, including its name, size, last modified time, and type (such as whether it is a file or folder) + ```go file, _ := ctx.File.Stat("my_file.text") entryType := "File" @@ -215,10 +282,12 @@ if entry.IsDir() { fmt.Printf("%v: %v Size: %v Last Modified Time : %v\n", entryType, entry.Name(), entry.Size(), entry.ModTime()) ``` ->Note: In S3: + +> Note: In S3 and GCS: +> > - Names without a file extension are treated as directories by default. -> - Names starting with "0" are interpreted as binary files, with the "0" prefix removed. -> +> - Names starting with "0" are interpreted as binary files, with the "0" prefix removed (S3 specific behavior). +> > For directories, the method calculates the total size of all contained objects and returns the most recent modification time. For files, it directly returns the file's size and last modified time. ### Rename/Move a File @@ -234,18 +303,23 @@ err := ctx.File.Rename("old_name.text", "new_name.text") ### Deleting Files `Remove` deletes a single file -> Note: Currently, the S3 package supports the deletion of unversioned files from general-purpose buckets only. Directory buckets and versioned files are not supported for deletion by this method. + +> Note: Currently, the S3 package supports the deletion of unversioned files from general-purpose buckets only. Directory buckets and versioned files are not supported for deletion by this method. GCS supports deletion of both files and empty directories. + ```go err := ctx.File.Remove("my_dir") ``` The `RemoveAll` command deletes all subdirectories as well. If you delete the current working directory, such as "../currentDir", the working directory will be reset to its parent directory. -> Note: In S3, RemoveAll only supports deleting directories and will return an error if a file path (as indicated by a file extension) is provided. + +> Note: In S3, RemoveAll only supports deleting directories and will return an error if a file path (as indicated by a file extension) is provided for S3. +> GCS handles both files and directories. + ```go err := ctx.File.RemoveAll("my_dir/my_text") ``` -> GoFr supports relative paths, allowing locations to be referenced relative to the current working directory. However, since S3 uses -> a flat file structure, all methods require a full path relative to the S3 bucket. +> GoFr supports relative paths, allowing locations to be referenced relative to the current working directory. However, since S3 and GCS use +> a flat file structure, all methods require a full path relative to the bucket. > Errors have been skipped in the example to focus on the core logic, it is recommended to handle all the errors. diff --git a/go.work b/go.work index e2667a918..0c75a4422 100644 --- a/go.work +++ b/go.work @@ -11,6 +11,7 @@ use ( ./pkg/gofr/datasource/elasticsearch ./pkg/gofr/datasource/file/ftp ./pkg/gofr/datasource/file/s3 + ./pkg/gofr/datasource/file/gcs ./pkg/gofr/datasource/file/sftp ./pkg/gofr/datasource/kv-store/badger ./pkg/gofr/datasource/kv-store/nats diff --git a/go.work.sum b/go.work.sum index da01c42da..2a6236ce4 100644 --- a/go.work.sum +++ b/go.work.sum @@ -9,7 +9,6 @@ cel.dev/expr v0.19.2 h1:V354PbqIXr9IQdwy4SYA4xa0HXaWq1BUPAGzugBY5V4= cel.dev/expr v0.19.2/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw= cel.dev/expr v0.20.0 h1:OunBvVCfvpWlt4dN7zg3FM6TDkzOePe1+foGJ9AXeeI= cel.dev/expr v0.20.0/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw= -cel.dev/expr v0.23.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY= cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= @@ -108,6 +107,8 @@ cloud.google.com/go/auth v0.12.1/go.mod h1:BFMu+TNpF3DmvfBO9ClqTR/SiqVIm7LukKF9m cloud.google.com/go/auth v0.13.0/go.mod h1:COOjD9gwfKNKz+IIduatIhYJQIc0mG3H102r/EMxX6Q= cloud.google.com/go/auth v0.14.1/go.mod h1:4JHUxlGXisL0AW8kXPtUF6ztuOksyfUQNFjfsOCXkPM= cloud.google.com/go/auth v0.15.0/go.mod h1:WJDGqZ1o9E9wKIL+IwStfyn/+s59zl4Bi+1KQNVXLZ8= +cloud.google.com/go/auth v0.16.0/go.mod h1:1howDHJ5IETh/LwYs3ZxvlkXF48aSqqJUM+5o02dNOI= +cloud.google.com/go/auth v0.16.1/go.mod h1:1howDHJ5IETh/LwYs3ZxvlkXF48aSqqJUM+5o02dNOI= cloud.google.com/go/auth/oauth2adapt v0.2.2/go.mod h1:wcYjgpZI9+Yu7LyYBg4pqSiaRkfEK3GQcpb7C/uyF1Q= cloud.google.com/go/automl v1.14.2/go.mod h1:mIat+Mf77W30eWQ/vrhjXsXaRh8Qfu4WiymR0hR6Uxk= cloud.google.com/go/automl v1.14.4 h1:vkD+hQ75SMINMgJBT/KDpFYvfQLzJbtIQZdw0AWq8Rs= @@ -388,6 +389,7 @@ cloud.google.com/go/gkemulticloud v1.5.3 h1:334aZmOzIt3LVBpguCof8IHaLaftcZlx+L0T cloud.google.com/go/gkemulticloud v1.5.3/go.mod h1:KPFf+/RcfvmuScqwS9/2MF5exZAmXSuoSLPuaQ98Xlk= cloud.google.com/go/grafeas v0.3.11 h1:CobnwnyeY1j1Defi5vbEircI+jfrk3ci5m004ZjiFP4= cloud.google.com/go/grafeas v0.3.11/go.mod h1:dcQyG2+T4tBgG0MvJAh7g2wl/xHV2w+RZIqivwuLjNg= +cloud.google.com/go/grafeas v0.3.15/go.mod h1:irwcwIQOBlLBotGdMwme8PipnloOPqILfIvMwlmu8Pk= cloud.google.com/go/gsuiteaddons v1.7.2/go.mod h1:GD32J2rN/4APilqZw4JKmwV84+jowYYMkEVwQEYuAWc= cloud.google.com/go/gsuiteaddons v1.7.3 h1:QafYhVhyFGpidBUUlVhy6lUHFogFOycVYm9DV7MinhA= cloud.google.com/go/gsuiteaddons v1.7.3/go.mod h1:0rR+LC21v1Sx1Yb6uohHI/F8DF3h2arSJSHvfi3GmyQ= @@ -426,9 +428,8 @@ cloud.google.com/go/lifesciences v0.10.3/go.mod h1:hnUUFht+KcZcliixAg+iOh88FUwAz cloud.google.com/go/lifesciences v0.10.6 h1:Vu7XF4s5KJ8+mSLIL4eaQM6JTyWXvSB54oqC+CUZH20= cloud.google.com/go/lifesciences v0.10.6/go.mod h1:1nnZwaZcBThDujs9wXzECnd1S5d+UiDkPuJWAmhRi7Q= cloud.google.com/go/logging v1.12.0/go.mod h1:wwYBt5HlYP1InnrtYI0wtwttpVU1rifnMT7RejksUAM= -cloud.google.com/go/logging v1.13.0 h1:7j0HgAp0B94o1YRDqiqm26w4q1rDMH7XNRU34lJXHYc= -cloud.google.com/go/logging v1.13.0/go.mod h1:36CoKh6KA/M0PbhPKMq6/qety2DCAErbhXT62TuXALA= cloud.google.com/go/longrunning v0.5.6/go.mod h1:vUaDrWYOMKRuhiv6JBnn49YxCPz2Ayn9GqyjaBT8/mA= +cloud.google.com/go/longrunning v0.6.6/go.mod h1:hyeGJUrPHcx0u2Uu1UFSoYZLn4lkMrccJig0t4FI7yw= cloud.google.com/go/managedidentities v1.7.2/go.mod h1:t0WKYzagOoD3FNtJWSWcU8zpWZz2i9cw2sKa9RiPx5I= cloud.google.com/go/managedidentities v1.7.3 h1:b9xGs24BIjfyvLgCtJoClOZpPi8d8owPgWe5JEINgaY= cloud.google.com/go/managedidentities v1.7.3/go.mod h1:H9hO2aMkjlpY+CNnKWRh+WoQiUIDO8457wWzUGsdtLA= @@ -467,8 +468,6 @@ cloud.google.com/go/monitoring v1.23.0 h1:M3nXww2gn9oZ/qWN2bZ35CjolnVHM3qnSbu6sr cloud.google.com/go/monitoring v1.23.0/go.mod h1:034NnlQPDzrQ64G2Gavhl0LUHZs9H3rRmhtnp7jiJgg= cloud.google.com/go/monitoring v1.24.0 h1:csSKiCJ+WVRgNkRzzz3BPoGjFhjPY23ZTcaenToJxMM= cloud.google.com/go/monitoring v1.24.0/go.mod h1:Bd1PRK5bmQBQNnuGwHBfUamAV1ys9049oEPHnn4pcsc= -cloud.google.com/go/monitoring v1.24.2 h1:5OTsoJ1dXYIiMiuL+sYscLc9BumrL3CarVLL7dd7lHM= -cloud.google.com/go/monitoring v1.24.2/go.mod h1:x7yzPWcgDRnPEv3sI+jJGBkwl5qINf+6qY4eq0I9B4U= cloud.google.com/go/networkconnectivity v1.15.2/go.mod h1:N1O01bEk5z9bkkWwXLKcN2T53QN49m/pSpjfUvlHDQY= cloud.google.com/go/networkconnectivity v1.16.1 h1:YsVhG71ZC4FkqCP2oCI55x/JeGFyd7738Lt8iNTrzJw= cloud.google.com/go/networkconnectivity v1.16.1/go.mod h1:GBC1iOLkblcnhcnfRV92j4KzqGBrEI6tT7LP52nZCTk= @@ -651,6 +650,7 @@ cloud.google.com/go/storage v1.43.0 h1:CcxnSohZwizt4LCzQHWvBf1/kvtHUn7gk9QERXPyX cloud.google.com/go/storage v1.43.0/go.mod h1:ajvxEa7WmZS1PxvKRq4bq0tFT3vMd502JwstCcYv0Q0= cloud.google.com/go/storage v1.50.0 h1:3TbVkzTooBvnZsk7WaAQfOsNrdoM8QHusXA1cpk6QJs= cloud.google.com/go/storage v1.50.0/go.mod h1:l7XeiD//vx5lfqE3RavfmU9yvk5Pp0Zhcv482poyafY= +cloud.google.com/go/storage v1.53.0/go.mod h1:7/eO2a/srr9ImZW9k5uufcNahT2+fPb8w5it1i5boaA= cloud.google.com/go/storagetransfer v1.11.2/go.mod h1:FcM29aY4EyZ3yVPmW5SxhqUdhjgPBUOFyy4rqiQbias= cloud.google.com/go/storagetransfer v1.12.1 h1:W3v9A7MGBN7H9sAFstyciwP/1XEQhUhZfrjclmDnpMs= cloud.google.com/go/storagetransfer v1.12.1/go.mod h1:hQqbfs8/LTmObJyCC0KrlBw8yBJ2bSFlaGila0qBMk4= @@ -684,8 +684,6 @@ cloud.google.com/go/tpu v1.8.3/go.mod h1:Do6Gq+/Jx6Xs3LcY2WhHyGwKDKVw++9jIJp+X+0 cloud.google.com/go/trace v1.11.2/go.mod h1:bn7OwXd4pd5rFuAnTrzBuoZ4ax2XQeG3qNgYmfCy0Io= cloud.google.com/go/trace v1.11.3 h1:c+I4YFjxRQjvAhRmSsmjpASUKq88chOX854ied0K/pE= cloud.google.com/go/trace v1.11.3/go.mod h1:pt7zCYiDSQjC9Y2oqCsh9jF4GStB/hmjrYLsxRR27q8= -cloud.google.com/go/trace v1.11.6 h1:2O2zjPzqPYAHrn3OKl029qlqG6W8ZdYaOWRyr8NgMT4= -cloud.google.com/go/trace v1.11.6/go.mod h1:GA855OeDEBiBMzcckLPE2kDunIpC72N+Pq8WFieFjnI= cloud.google.com/go/translate v1.10.3/go.mod h1:GW0vC1qvPtd3pgtypCv4k4U8B7EdgK9/QEF2aJEUovs= cloud.google.com/go/translate v1.12.2/go.mod h1:jjLVf2SVH2uD+BNM40DYvRRKSsuyKxVvs3YjTW/XSWY= cloud.google.com/go/translate v1.12.3 h1:XJ7LipYJi80BCgVk2lx1fwc7DIYM6oV2qx1G4IAGQ5w= @@ -766,8 +764,6 @@ github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.25.0 github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.25.0/go.mod h1:obipzmGjfSjam60XLwGfqUkJsfiheAl+TUjG+4yzyPM= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.26.0 h1:f2Qw/Ehhimh5uO1fayV0QIW7DShEQqhtUfhYc+cBPlw= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.26.0/go.mod h1:2bIszWvQRlJVmJLiuLhukLImRjKPcYdzzsx6darK02A= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0 h1:ErKg/3iS1AKcTkf3yixlZ54f9U1rljCkQyEXWUnIUxc= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0/go.mod h1:yAZHSGnqScoU556rBOVkwLze6WP5N+U11RHuWaGVxwY= github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.49.0 h1:o90wcURuxekmXrtxmYWTyNla0+ZEHhud6DI1ZTxd1vI= github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.49.0/go.mod h1:6fTWu4m3jocfUZLYF5KsZC1TUfRvEjs7lM4crme/irw= github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.50.0 h1:5IT7xOdq17MtcdtL/vtl6mGfzhaq4m4vpollPRmlsBQ= @@ -831,8 +827,6 @@ github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78/go.mod h1:W+zGtBO5Y1Ig github.com/cncf/xds/go v0.0.0-20241223141626-cff3c89139a3/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= github.com/cncf/xds/go v0.0.0-20250121191232-2f005788dc42 h1:Om6kYQYDUk5wWbT0t0q6pvyM49i9XZAv9dDrkDA7gjk= github.com/cncf/xds/go v0.0.0-20250121191232-2f005788dc42/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= -github.com/cncf/xds/go v0.0.0-20250326154945-ae57f3c0d45f h1:C5bqEmzEPLsHm9Mv73lSE9e9bKV23aB1vxOsmZrkl3k= -github.com/cncf/xds/go v0.0.0-20250326154945-ae57f3c0d45f/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 h1:aQ3y1lwWyqYPiWZThqv1aFbZMiM9vblcSArJRf2Irls= github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= github.com/containerd/containerd v1.7.12 h1:+KQsnv4VnzyxWcfO9mlxxELaoztsDEjOuCMPAuPqgU0= @@ -889,19 +883,11 @@ github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5y github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/go-control-plane v0.13.1 h1:vPfJZCkob6yTMEgS+0TwfTUfbHjfy/6vOJ8hUWX/uXE= github.com/envoyproxy/go-control-plane v0.13.1/go.mod h1:X45hY0mufo6Fd0KW3rqsGvQMw58jvjymeCzBU3mWyHw= -github.com/envoyproxy/go-control-plane v0.13.4 h1:zEqyPVyku6IvWCFwux4x9RxkLOMUL+1vC9xUFv5l2/M= -github.com/envoyproxy/go-control-plane v0.13.4/go.mod h1:kDfuBlDVsSj2MjrLEtRWtHlsWIFcGyB2RMO44Dc5GZA= github.com/envoyproxy/go-control-plane/envoy v1.32.3 h1:hVEaommgvzTjTd4xCaFd+kEQ2iYBtGxP6luyLrx6uOk= github.com/envoyproxy/go-control-plane/envoy v1.32.3/go.mod h1:F6hWupPfh75TBXGKA++MCT/CZHFq5r9/uwt/kQYkZfE= -github.com/envoyproxy/go-control-plane/envoy v1.32.4 h1:jb83lalDRZSpPWW2Z7Mck/8kXZ5CQAFYVjQcdVIr83A= -github.com/envoyproxy/go-control-plane/envoy v1.32.4/go.mod h1:Gzjc5k8JcJswLjAx1Zm+wSYE20UrLtt7JZMWiWQXQEw= -github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI= -github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4= github.com/envoyproxy/protoc-gen-validate v1.0.4/go.mod h1:qys6tmnRsYrQqIhm2bvKZH4Blx/1gTIZ2UKVY1M+Yew= github.com/envoyproxy/protoc-gen-validate v1.1.0 h1:tntQDh69XqOCOZsDz0lVJQez/2L6Uu2PdjCQwWCJ3bM= github.com/envoyproxy/protoc-gen-validate v1.1.0/go.mod h1:sXRDRVmzEbkM7CVcM06s9shE/m23dg3wzjl0UWqJ2q4= -github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8= -github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1 h1:QbL/5oDUmRBzO9/Z7Seo6zf912W/a6Sr4Eu0G/3Jho0= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= @@ -909,8 +895,6 @@ github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4 h1:WtGNWLvXpe github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-jose/go-jose/v4 v4.0.4 h1:VsjPI33J0SB9vQM6PLmNjoHqMQNGPiZ0rHL7Ni7Q6/E= github.com/go-jose/go-jose/v4 v4.0.4/go.mod h1:NKb5HO1EZccyMpiZNbdUw/14tiXNyUJh188dfnMCAfc= -github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE= -github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA= github.com/go-kit/log v0.1.0 h1:DGJh0Sm43HbOeYDNnVZFl8BvcYVvjD5bqYJvp0REbwQ= github.com/go-kit/log v0.2.1 h1:MRVx0/zhvdseW+Gza6N9rVzU/IVzaeE1SFI4raAhmBU= github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= @@ -976,8 +960,6 @@ github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPg github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= -github.com/google/martian/v3 v3.3.3 h1:DIhPTQrbPkgs2yJYdXU/eNACCG5DVQjySNRNlflZ9Fc= -github.com/google/martian/v3 v3.3.3/go.mod h1:iEPrYcgCF7jA9OtScMFQyAlZZ4YXTKEtJ1E6RWzmBA0= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= @@ -1001,6 +983,7 @@ github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+ github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/gax-go/v2 v2.12.3/go.mod h1:AKloxT6GtNbaLm8QTNSidHUVsHYcBHwWRvkNFJUQcS4= github.com/googleapis/gax-go/v2 v2.14.0/go.mod h1:lhBCnjdLrWRaPvLWhmc8IS24m9mr07qSYnHncrgo+zk= +github.com/googleapis/gax-go/v2 v2.14.1/go.mod h1:Hb/NubMaVM88SrNkvl8X/o8XWwDJEPqouaLeN2IUxoA= github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8 h1:tlyzajkF3030q6M8SvmJSemC9DTHL/xaMa18b65+JM4= github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= @@ -1120,8 +1103,6 @@ github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2D github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e h1:aoZm08cpOy4WuID//EZDgcC4zIxODThtZNPirFr42+A= github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= -github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= -github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/prometheus/client_golang v1.20.4/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= @@ -1153,11 +1134,8 @@ github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spiffe/go-spiffe/v2 v2.5.0 h1:N2I01KCUkv1FAjZXJMwh95KK1ZIQLYbPfhaxw8WS0hE= -github.com/spiffe/go-spiffe/v2 v2.5.0/go.mod h1:P+NxobPc6wXhVtINNtFjNWGBTreew1GBUCwT2wPmb7g= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/testcontainers/testcontainers-go v0.28.0 h1:1HLm9qm+J5VikzFDYhOd+Zw12NtOl+8drH2E8nTY1r8= github.com/testcontainers/testcontainers-go v0.28.0/go.mod h1:COlDpUXbwW3owtpMkEB1zo9gwb1CoKVKlyrVPejF4AU= github.com/testcontainers/testcontainers-go v0.33.0 h1:zJS9PfXYT5O0ZFXM2xxXfk4J5UMw/kRiISng037Gxdw= @@ -1175,13 +1153,12 @@ github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8 github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13 h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE= github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw= github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= -github.com/zeebo/errs v1.4.0 h1:XNdoD/RRMKP7HD0UhJnIzUy74ISdGGxURlYG8HSWSfM= -github.com/zeebo/errs v1.4.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4= github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= @@ -1199,14 +1176,14 @@ go.opentelemetry.io/contrib/detectors/gcp v1.34.0 h1:JRxssobiPg23otYU5SbWtQC//sn go.opentelemetry.io/contrib/detectors/gcp v1.34.0/go.mod h1:cV4BMFcscUR/ckqLkbfQmF0PRsq8w/lMGzdbCSveBHo= go.opentelemetry.io/contrib/detectors/gcp v1.35.0 h1:bGvFt68+KTiAKFlacHW6AhA56GF2rS0bdD3aJYEnmzA= go.opentelemetry.io/contrib/detectors/gcp v1.35.0/go.mod h1:qGWP8/+ILwMRIUf9uIVLloR1uo5ZYAslM4O6OqUi1DA= -go.opentelemetry.io/contrib/detectors/gcp v1.36.0 h1:F7q2tNlCaHY9nMKHR6XH9/qkp8FktLnIcy6jJNyOCQw= -go.opentelemetry.io/contrib/detectors/gcp v1.36.0/go.mod h1:IbBN8uAIIx734PTonTPxAxnjc2pQTxWNkwfstZ+6H2k= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0/go.mod h1:B9yO6b04uB80CzjedvewuqDhxJxi11s7/GtiGa8bAjI= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0/go.mod h1:ijPqXp5P6IRRByFVVg9DY8P5HkxkHE5ARIa+86aXPf4= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0/go.mod h1:rg+RlpR5dKwaS95IyyZqj5Wd4E13lk/msnTS0Xl9lJM= go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.56.0/go.mod h1:3qi2EEwMgB4xnKgPLqsDP3j9qxnHDZeHsnAxfjQqTko= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0/go.mod h1:umTcuxiv1n/s/S6/c2AT/g2CQ7u5C59sHDNmfSwgz7Q= +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.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q= go.opentelemetry.io/otel v1.22.0/go.mod h1:eoV4iAi3Ea8LkAEI9+GFT44O6T/D0GWAVFyZVCC6pMI= go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo= @@ -1233,7 +1210,9 @@ go.opentelemetry.io/otel/sdk v1.22.0/go.mod h1:iu7luyVGYovrRpe2fmj3CVKouQNdTOkxt go.opentelemetry.io/otel/sdk v1.29.0/go.mod h1:pM8Dx5WKnvxLCb+8lG1PRNIDxu9g9b9g59Qr7hfAAok= go.opentelemetry.io/otel/sdk v1.32.0/go.mod h1:LqgegDBjKMmb2GC6/PrTnteJG39I8/vJCAP9LlJXEjU= go.opentelemetry.io/otel/sdk v1.33.0/go.mod h1:A1Q5oi7/9XaMlIWzPSxLRWOI8nG3FnzHJNbiENQuihM= +go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg= go.opentelemetry.io/otel/sdk/metric v1.32.0/go.mod h1:PWeZlq0zt9YkYAp3gjKZ0eicRYvOh1Gd+X99x6GHpCQ= +go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w= go.opentelemetry.io/otel/trace v1.22.0/go.mod h1:RbbHXVqKES9QhzZq/fE5UnOSILqRt40a21sPw2He1xo= go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ= @@ -1255,8 +1234,10 @@ golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= +golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= +golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= @@ -1291,6 +1272,7 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= @@ -1319,6 +1301,7 @@ golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81R golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= @@ -1329,7 +1312,7 @@ golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= -golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= +golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -1358,7 +1341,6 @@ golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= -golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -1387,6 +1369,7 @@ golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= @@ -1405,6 +1388,7 @@ golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fq golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= @@ -1415,6 +1399,7 @@ golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxb golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= @@ -1456,6 +1441,7 @@ golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools v0.1.8/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c= @@ -1493,6 +1479,10 @@ google.golang.org/api v0.219.0/go.mod h1:K6OmjGm+NtLrIkHxv1U3a0qIf/0JOvAHd5O/6Ao google.golang.org/api v0.222.0/go.mod h1:efZia3nXpWELrwMlN5vyQrD4GmJN1Vw0x68Et3r+a9c= google.golang.org/api v0.224.0/go.mod h1:3V39my2xAGkodXy0vEqcEtkqgw2GtrFL5WuBZlCTCOQ= google.golang.org/api v0.227.0/go.mod h1:EIpaG6MbTgQarWF5xJvX0eOJPK9n/5D4Bynb9j2HXvQ= +google.golang.org/api v0.229.0/go.mod h1:wyDfmq5g1wYJWn29O22FDWN48P7Xcz0xz+LBpptYvB0= +google.golang.org/api v0.232.0/go.mod h1:p9QCfBWZk1IJETUdbTKloR5ToFdKbYh2fkjsUL6vNoY= +google.golang.org/api v0.235.0/go.mod h1:QpeJkemzkFKe5VCE/PMv7GsUfn9ZF+u+q1Q7w6ckxTg= +google.golang.org/api v0.239.0/go.mod h1:cOVEm2TpdAGHL2z+UwyS+kmlGr3bVWQQ6sYEqkKje50= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= @@ -1535,6 +1525,7 @@ google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6D google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20240711142825-46eb208f015d/go.mod h1:FfBgJBJg9GcpPvKIuHSZ/aE1g2ecGL74upMzGZjiGEY= google.golang.org/genproto v0.0.0-20250122153221-138b5a5a4fd4/go.mod h1:qbZzneIOXSq+KFAFut9krLfRLZiFLzZL5u2t8SV83EE= +google.golang.org/genproto v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:sAo5UzpjUwgFBCzupwhcLcxHVDK7vG5IqI30YnwX2eE= google.golang.org/genproto/googleapis/api v0.0.0-20230822172742-b8732ec3820d/go.mod h1:KjSP20unUpOx5kyQUFa7k4OJg0qeJ7DEZflGDu2p6Bk= google.golang.org/genproto/googleapis/api v0.0.0-20240429193739-8cf5692501f6/go.mod h1:10yRODfgim2/T8csjQsMPgZOMvtytXKTDRzH6HRGzRw= google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142/go.mod h1:d6be+8HhtEtucleCbxpPW9PA9XwISACu8nvpPqF0BVo= @@ -1550,6 +1541,9 @@ google.golang.org/genproto/googleapis/api v0.0.0-20250219182151-9fdb1cabc7b2/go. google.golang.org/genproto/googleapis/api v0.0.0-20250227231956-55c901821b1e/go.mod h1:Xsh8gBVxGCcbV8ZeTB9wI5XPyZ5RvC6V3CTeeplHbiA= google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:jbe3Bkdp+Dh2IrslsFCklNhweNTBgSYanP1UXhJDhKg= google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463/go.mod h1:U90ffi8eUL9MwPcrJylN5+Mk2v3vuPDptd5yyNUiRR8= +google.golang.org/genproto/googleapis/api v0.0.0-20250414145226-207652e42e2e/go.mod h1:085qFyf2+XaZlRdCgKNCIZ3afY2p4HHZdoIRpId8F4A= +google.golang.org/genproto/googleapis/api v0.0.0-20250425173222-7b384671a197/go.mod h1:Cd8IzgPo5Akum2c9R6FsXNaZbH3Jpa2gpHlW89FqlyQ= +google.golang.org/genproto/googleapis/api v0.0.0-20250512202823-5a2f75b736a9/go.mod h1:W3S/3np0/dPWsWLi1h/UymYctGXaGBM2StwzD0y140U= google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a/go.mod h1:a77HrdMjoeKbnd2jmgcWdaS++ZLZAEq3orIOAEIKiVw= google.golang.org/genproto/googleapis/bytestream v0.0.0-20250106144421-5f5ef82da422 h1:w6g+P/ZscmNlGxVVXGaPVQOLu1q19ubsTOZKwaDqm4k= google.golang.org/genproto/googleapis/bytestream v0.0.0-20250106144421-5f5ef82da422/go.mod h1:s4mHJ3FfG8P6A3O+gZ8TVqB3ufjOl9UG3ANCMMwCHmo= @@ -1587,6 +1581,8 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20250219182151-9fdb1cabc7b2/go. google.golang.org/genproto/googleapis/rpc v0.0.0-20250227231956-55c901821b1e/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I= 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-20250313205543-e70fdf4c4cb4/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250414145226-207652e42e2e/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250505200425-f936aa4a68b2/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= google.golang.org/genproto/googleapis/rpc v0.0.0-20250512202823-5a2f75b736a9/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= @@ -1607,6 +1603,7 @@ google.golang.org/grpc v1.68.1/go.mod h1:+q1XYFJjShcqn0QZHvCyeR4CXPA+llXIeUIfIe0 google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw= google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= google.golang.org/grpc v1.71.1/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= +google.golang.org/grpc v1.72.0/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= google.golang.org/grpc v1.72.1/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= google.golang.org/grpc v1.72.2/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= google.golang.org/grpc/examples v0.0.0-20230224211313-3775f633ce20 h1:MLBCGN1O7GzIx+cBiwfYPwtmZ41U3Mn/cotLJciaArI= diff --git a/pkg/gofr/datasource/file/gcs/file.go b/pkg/gofr/datasource/file/gcs/file.go new file mode 100644 index 000000000..0edf7317f --- /dev/null +++ b/pkg/gofr/datasource/file/gcs/file.go @@ -0,0 +1,149 @@ +package gcs + +import ( + "context" + "errors" + "io" + "time" + + "cloud.google.com/go/storage" +) + +type File struct { + conn gcsClient + writer *storage.Writer + name string + logger Logger + metrics Metrics + size int64 + contentType string + body io.ReadCloser + lastModified time.Time + isDir bool +} + +var ( + errNilGCSFileBody = errors.New("GCS file body is nil") + errSeekNotSupported = errors.New("seek not supported on GCSFile") + errReadAtNotSupported = errors.New("readAt not supported on GCSFile") + errWriteAtNotSupported = errors.New("writeAt not supported on GCSFile (read-only)") +) + +const ( + msgWriterClosed = "Writer closed successfully" + msgReaderClosed = "Reader closed successfully" +) + +// ====== File interface methods ====== + +func (f *File) Read(p []byte) (int, error) { + if f.body == nil { + return 0, errNilGCSFileBody + } + + return f.body.Read(p) +} +func (f *File) Write(p []byte) (int, error) { + bucketName := getBucketName(f.name) + + var msg string + + st := statusErr + + defer f.sendOperationStats(&FileLog{ + Operation: "WRITE", + Location: getLocation(bucketName), + Status: &st, + Message: &msg, + }, time.Now()) + + n, err := f.writer.Write(p) + + if err != nil { + f.logger.Errorf("failed to write: %v", err) + msg = err.Error() + + return n, err + } + + st, msg = statusSuccess, "Write successful" + f.logger.Debug(msg) + + return n, nil +} + +func (f *File) Close() error { + bucketName := getBucketName(f.name) + + var msg string + + st := statusErr + + defer f.sendOperationStats(&FileLog{ + Operation: "CLOSE", + Location: getLocation(bucketName), + Status: &st, + Message: &msg, + }, time.Now()) + + if f.writer != nil { + err := f.writer.Close() + if err != nil { + msg = err.Error() + return err + } + + st = statusSuccess + + msg = msgWriterClosed + + f.logger.Debug(msg) + + return nil + } + + if f.body != nil { + err := f.body.Close() + if err != nil { + msg = err.Error() + return err + } + + st = statusSuccess + + msg = msgReaderClosed + + f.logger.Debug(msgReaderClosed) + + return nil + } + + st = statusSuccess + + msg = msgWriterClosed + + return nil +} + +func (*File) Seek(_ int64, _ int) (int64, error) { + // Not supported: Seek requires reopening with range. + return 0, errSeekNotSupported +} + +func (*File) ReadAt(_ []byte, _ int64) (int, error) { + return 0, errReadAtNotSupported +} + +func (*File) WriteAt(_ []byte, _ int64) (int, error) { + return 0, errWriteAtNotSupported +} + +func (f *File) sendOperationStats(fl *FileLog, startTime time.Time) { + duration := time.Since(startTime).Microseconds() + + fl.Duration = duration + + f.logger.Debug(fl) + f.metrics.RecordHistogram(context.Background(), appFTPStats, float64(duration), + "type", fl.Operation, "status", clean(fl.Status)) +} diff --git a/pkg/gofr/datasource/file/gcs/file_parse.go b/pkg/gofr/datasource/file/gcs/file_parse.go new file mode 100644 index 000000000..e700e8fba --- /dev/null +++ b/pkg/gofr/datasource/file/gcs/file_parse.go @@ -0,0 +1,206 @@ +package gcs + +import ( + "bufio" + "bytes" + "encoding/json" + "errors" + "io" + "io/fs" + "path" + "strings" + "time" + + "gofr.dev/pkg/gofr/datasource/file" +) + +var ( + // errNotPointer is returned when Read method is called with a non-pointer argument. + errStringNotPointer = errors.New("input should be a pointer to a string") +) + +const ( + appFTPStats = "app_ftp_stats" + statusErr = "ERROR" + statusSuccess = "SUCCESS" +) + +// textReader implements RowReader for reading text files. +type textReader struct { + scanner *bufio.Scanner + logger Logger +} + +// jsonReader implements RowReader for reading JSON files. +type jsonReader struct { + decoder *json.Decoder + token json.Token +} + +func (f *File) ReadAll() (file.RowReader, error) { + bucketName := getBucketName(f.name) + location := path.Join(bucketName, f.name) + + defer f.sendOperationStats(&FileLog{ + Operation: "READALL", + Location: location, + }, time.Now()) + + if strings.HasSuffix(f.Name(), ".json") { + return f.createJSONReader(location) + } + + return f.createTextCSVReader(location) +} + +// createJSONReader creates a JSON reader for JSON files. +func (f *File) createJSONReader(location string) (file.RowReader, error) { + status := statusErr + + defer f.sendOperationStats(&FileLog{Operation: "JSON READER", Location: location, Status: &status}, time.Now()) + + buffer, err := io.ReadAll(f.body) + if err != nil { + f.logger.Errorf("ReadAll Failed: Unable to read json file: %v", err) + return nil, err + } + + reader := bytes.NewReader(buffer) + + decoder := json.NewDecoder(reader) + + // Peek the first JSON token to determine the type + // Note: This results in offset to move ahead, making it necessary to + // decode again if we are decoding a json object instead of array + token, err := decoder.Token() + if err != nil { + f.logger.Errorf("Error decoding token: %v", err) + return nil, err + } + + if d, ok := token.(json.Delim); ok && d == '[' { + status = statusSuccess + return &jsonReader{decoder: decoder, token: token}, err + } + + // Reading JSON object + decoder = json.NewDecoder(reader) + status = statusSuccess + + return &jsonReader{decoder: decoder}, nil +} + +// createTextCSVReader creates a text reader for reading text files. +func (f *File) createTextCSVReader(location string) (file.RowReader, error) { + status := statusErr + + defer f.sendOperationStats(&FileLog{Operation: "TEXT/CSV READER", Location: location, Status: &status}, time.Now()) + + buffer, err := io.ReadAll(f.body) + if err != nil { + f.logger.Errorf("ReadAll failed: Unable to read text file: %v", err) + return nil, err + } + + reader := bytes.NewReader(buffer) + status = statusSuccess + + return &textReader{ + scanner: bufio.NewScanner(reader), + logger: f.logger, + }, err +} + +func (j *jsonReader) Next() bool { + return j.decoder.More() +} + +// Scan decodes the next JSON object into the provided structure. +func (j *jsonReader) Scan(i any) error { + return j.decoder.Decode(&i) +} + +// Next checks if there is another line available in the text file. +func (f *textReader) Next() bool { + return f.scanner.Scan() +} + +// Scan scans the next line from the text file into the provided pointer to strinf. +func (f *textReader) Scan(i any) error { + if val, ok := i.(*string); ok { + *val = f.scanner.Text() + return nil + } + + return errStringNotPointer +} + +func (f *File) Name() string { + bucketName := getBucketName(f.name) + + f.sendOperationStats(&FileLog{ + Operation: "GET NAME", + Location: getLocation(bucketName), + }, time.Now()) + + return f.name +} + +func (f *File) Size() int64 { + bucketName := getBucketName(f.name) + + f.sendOperationStats(&FileLog{ + Operation: "FILE/DIR SIZE", + Location: getLocation(bucketName), + }, time.Now()) + + return f.size +} + +func (f *File) ModTime() time.Time { + bucketName := getBucketName(f.name) + + f.sendOperationStats(&FileLog{ + Operation: "LAST MODIFIED", + Location: getLocation(bucketName), + }, time.Now()) + + return f.lastModified +} + +func (f *File) Mode() fs.FileMode { + bucketName := getBucketName(f.name) + + f.sendOperationStats(&FileLog{ + Operation: "MODE", + Location: getLocation(bucketName), + }, time.Now()) + + if f.isDir { + return fs.ModeDir + } + + return 0 +} + +func (f *File) IsDir() bool { + bucketName := getBucketName(f.name) + + f.sendOperationStats(&FileLog{ + Operation: "IS DIR", + Location: getLocation(bucketName), + }, time.Now()) + + return f.isDir || f.contentType == "application/x-directory" +} + +func (f *File) Sys() any { + bucketName := getBucketName(f.name) + + f.sendOperationStats(&FileLog{ + Operation: "SYS", + Location: getLocation(bucketName), + }, time.Now()) + + return nil +} diff --git a/pkg/gofr/datasource/file/gcs/fs.go b/pkg/gofr/datasource/file/gcs/fs.go new file mode 100644 index 000000000..7c5c85f48 --- /dev/null +++ b/pkg/gofr/datasource/file/gcs/fs.go @@ -0,0 +1,323 @@ +package gcs + +import ( + "context" + "errors" + "fmt" + "os" + "path" + "strings" + "time" + + "cloud.google.com/go/storage" + file "gofr.dev/pkg/gofr/datasource/file" + "google.golang.org/api/option" +) + +var ( + errOperationNotPermitted = errors.New("operation not permitted") + errWriterTypeAssertion = errors.New("writer is not of type *storage.Writer") +) + +type FileSystem struct { + GCSFile File + conn gcsClient + config *Config + logger Logger + metrics Metrics +} + +// Config represents the gcs configuration. +type Config struct { + EndPoint string + BucketName string + CredentialsJSON string + ProjectID string +} + +// New initializes a new instance of FTP fileSystem with provided configuration. +func New(config *Config) file.FileSystemProvider { + return &FileSystem{config: config} +} + +func (f *FileSystem) Connect() { + var msg string + + st := statusErr + + defer f.sendOperationStats(&FileLog{ + Operation: "CONNECT", + Location: getLocation(f.config.BucketName), + Status: &st, + Message: &msg, + }, time.Now()) + + f.logger.Debugf("connecting to GCS bucket: %s", f.config.BucketName) + + ctx := context.TODO() + + var client *storage.Client + + var err error + + switch { + case f.config.EndPoint != "": + // Local emulator mode + client, err = storage.NewClient( + ctx, + option.WithEndpoint(f.config.EndPoint), + option.WithoutAuthentication(), + ) + + case f.config.CredentialsJSON != "": + // Direct JSON mode + client, err = storage.NewClient( + ctx, + option.WithCredentialsJSON([]byte(f.config.CredentialsJSON)), + ) + + default: + // Env var mode (GOOGLE_APPLICATION_CREDENTIALS) + client, err = storage.NewClient(ctx) + } + + if err != nil { + f.logger.Errorf("Failed to connect to GCS: %v", err) + return + } + + f.conn = &gcsClientImpl{ + client: client, + bucket: client.Bucket(f.config.BucketName), + } + + st = statusSuccess + msg = "GCS Client connected." + + f.logger.Logf("connected to GCS bucket %s", f.config.BucketName) +} + +func (f *FileSystem) Create(name string) (file.File, error) { + var ( + msg string + st = statusErr + ) + + startTime := time.Now() + defer f.sendOperationStats(&FileLog{ + Operation: "CREATE FILE", + Location: getLocation(f.config.BucketName), + Status: &st, + Message: &msg, + }, startTime) + + ctx := context.Background() + + // 1. Check if parent directory exists + parentPath := path.Dir(name) + checkPath := "." + + if parentPath != "." { + checkPath = parentPath + "/" + } + + if _, err := f.conn.ListObjects(ctx, checkPath); err != nil { + msg = "Parent directory does not exist" + + f.logger.Errorf("Failed to list parent directory %q: %v", checkPath, err) + + return nil, err + } + + // 2. Resolve file name conflict + originalName := name + + for index := 1; ; index++ { + objs, err := f.conn.ListObjects(ctx, name) + + if err != nil { + msg = "Error checking existing objects" + + f.logger.Errorf("Failed to list objects for name %q: %v", name, err) + + return nil, err + } + + if len(objs) == 0 { + break // Safe to use + } + + name = generateCopyName(originalName, index) + } + + // 3. Open writer to create file + writer := f.conn.NewWriter(ctx, name) + + sw, ok := writer.(*storage.Writer) + if !ok { + msg = "Failed to assert writer to *storage.Writer" + + f.logger.Errorf("Type assertion failed for writer to *storage.Writer") + + return nil, fmt.Errorf("type assertion failed: %w", errWriterTypeAssertion) + } + + st = statusSuccess + msg = "Write stream opened successfully" + + f.logger.Logf("Write stream successfully opened for file %q", name) + + return &File{ + conn: f.conn, + writer: sw, + name: name, + contentType: sw.ContentType, + size: sw.Size, + lastModified: sw.Updated, + logger: f.logger, + metrics: f.metrics, + }, nil +} + +func (f *FileSystem) Remove(name string) error { + var msg string + + st := statusErr + + defer f.sendOperationStats(&FileLog{ + Operation: "REMOVE FILE", + Location: getLocation(f.config.BucketName), + Status: &st, + Message: &msg, + }, time.Now()) + + ctx := context.TODO() + err := f.conn.DeleteObject(ctx, name) + + if err != nil { + f.logger.Errorf("Error while deleting file: %v", err) + return err + } + + st = statusSuccess + msg = "File deletion on GCS successful" + + f.logger.Logf("File with path %q deleted", name) + + return nil +} + +func (f *FileSystem) Open(name string) (file.File, error) { + var msg string + + st := statusErr + + defer f.sendOperationStats(&FileLog{ + Operation: "OPEN FILE", + Location: getLocation(f.config.BucketName), + Status: &st, + Message: &msg, + }, time.Now()) + + ctx := context.TODO() + + reader, err := f.conn.NewReader(ctx, name) + + if err != nil { + if errors.Is(err, storage.ErrObjectNotExist) { + return nil, file.ErrFileNotFound + } + + f.logger.Errorf("failed to retrieve %q: %v", name, err) + + return nil, err + } + + attr, err := f.conn.StatObject(ctx, name) + if err != nil { + reader.Close() + return nil, err + } + + st = statusSuccess + + msg = fmt.Sprintf("File with path %q retrieved successfully", name) + + return &File{ + conn: f.conn, + name: name, + body: reader, + logger: f.logger, + metrics: f.metrics, + size: attr.Size, + contentType: attr.ContentType, + lastModified: attr.Updated, + }, nil +} + +func (f *FileSystem) Rename(oldname, newname string) error { + var msg string + + st := statusErr + + defer f.sendOperationStats(&FileLog{ + Operation: "RENAME", + Location: getLocation(f.config.BucketName), + Status: &st, + Message: &msg, + }, time.Now()) + + ctx := context.TODO() + + if oldname == newname { + f.logger.Logf("%q & %q are same", oldname, newname) + return nil + } + + if path.Dir(oldname) != path.Dir(newname) { + f.logger.Errorf("%q & %q are not in same location", oldname, newname) + return fmt.Errorf("%w: renaming as well as moving file to different location is not allowed", errOperationNotPermitted) + } + // Copy old object to new + if err := f.conn.CopyObject(ctx, oldname, newname); err != nil { + msg = fmt.Sprintf("Error while copying file: %v", err) + return err + } + + // Delete old + err := f.conn.DeleteObject(ctx, oldname) + if err != nil { + msg = fmt.Sprintf("failed to remove old file %s", oldname) + return err + } + + st = statusSuccess + msg = "File renamed successfully" + + f.logger.Logf("File with path %q renamed to %q", oldname, newname) + + return nil +} +func (f *FileSystem) OpenFile(name string, _ int, _ os.FileMode) (file.File, error) { + return f.Open(name) +} + +// UseLogger sets the Logger interface for the FTP file system. +func (f *FileSystem) UseLogger(logger any) { + if l, ok := logger.(Logger); ok { + f.logger = l + } +} + +// UseMetrics sets the Metrics interface. +func (f *FileSystem) UseMetrics(metrics any) { + if m, ok := metrics.(Metrics); ok { + f.metrics = m + } +} +func generateCopyName(original string, count int) string { + ext := path.Ext(original) + base := strings.TrimSuffix(original, ext) + + return fmt.Sprintf("%s copy %d%s", base, count, ext) +} diff --git a/pkg/gofr/datasource/file/gcs/fs_dir.go b/pkg/gofr/datasource/file/gcs/fs_dir.go new file mode 100644 index 000000000..ce9f77ec7 --- /dev/null +++ b/pkg/gofr/datasource/file/gcs/fs_dir.go @@ -0,0 +1,307 @@ +package gcs + +import ( + "context" + "errors" + "fmt" + "os" + "path" + "path/filepath" + "strings" + "time" + + "cloud.google.com/go/storage" + file "gofr.dev/pkg/gofr/datasource/file" + "google.golang.org/api/googleapi" +) + +var ( + errEmptyDirectoryName = errors.New("directory name cannot be empty") + errCHNDIRNotSupported = errors.New("changing directory is not supported in GCS") +) + +func getBucketName(filePath string) string { + return strings.Split(filePath, string(filepath.Separator))[0] +} + +func getLocation(bucket string) string { + return path.Join(string(filepath.Separator), bucket) +} + +func (f *FileSystem) Mkdir(name string, _ os.FileMode) error { + var msg string + + st := statusErr + + defer f.sendOperationStats(&FileLog{ + Operation: "MKDIR", + Location: getLocation(f.config.BucketName), + Status: &st, + Message: &msg, + }, time.Now()) + + if name == "" { + msg = "directory name cannot be empty" + f.logger.Errorf(msg) + + return errEmptyDirectoryName + } + + ctx := context.TODO() + objName := name + "/" + + writer := f.conn.NewWriter(ctx, objName) + defer writer.Close() + + _, err := writer.Write([]byte("dir")) + if err != nil { + if err != nil { + msg = fmt.Sprintf("failed to create directory %q on GCS: %v", objName, err) + f.logger.Errorf(msg) + + return err + } + } + + st = statusSuccess + + msg = fmt.Sprintf("Directories on path %q created successfully", name) + + f.logger.Logf("Created directories on path %q", name) + + return err +} + +func (f *FileSystem) MkdirAll(dirPath string, perm os.FileMode) error { + cleaned := strings.Trim(dirPath, "/") + if cleaned == "" { + return nil + } + + dirs := strings.Split(cleaned, "/") + + var currentPath string + + for _, dir := range dirs { + currentPath = pathJoin(currentPath, dir) + err := f.Mkdir(currentPath, perm) + + if err != nil && !isAlreadyExistsError(err) { + return err + } + } + + return nil +} +func pathJoin(parts ...string) string { + return path.Join(parts...) +} + +func isAlreadyExistsError(err error) bool { + var gErr *googleapi.Error + if errors.As(err, &gErr) { + return gErr.Code == 409 || gErr.Code == 412 + } + + // Fallback check + return strings.Contains(err.Error(), "already exists") +} + +func (f *FileSystem) RemoveAll(dirPath string) error { + var msg string + + st := statusErr + + defer f.sendOperationStats(&FileLog{ + Operation: "REMOVEALL", + Location: getLocation(f.config.BucketName), + Status: &st, + Message: &msg, + }, time.Now()) + + ctx := context.TODO() + objects, err := f.conn.ListObjects(ctx, dirPath) + + if err != nil { + msg = fmt.Sprintf("Error retrieving objects: %v", err) + return err + } + + for _, obj := range objects { + if err := f.conn.DeleteObject(ctx, obj); err != nil { + f.logger.Errorf("Error while deleting directory: %v", err) + return err + } + } + + st = statusSuccess + + msg = fmt.Sprintf("Directory with path %q, deleted successfully", dirPath) + + f.logger.Logf("Directory %s deleted.", dirPath) + + return nil +} + +func (f *FileSystem) ReadDir(dir string) ([]file.FileInfo, error) { + var msg string + + st := statusErr + + defer f.sendOperationStats(&FileLog{ + Operation: "READDIR", + Location: getLocation(f.config.BucketName), + Status: &st, + Message: &msg, + }, time.Now()) + + ctx := context.TODO() + + objects, prefixes, err := f.conn.ListDir(ctx, dir) + if err != nil { + msg = fmt.Sprintf("Error retrieving objects: %v", err) + f.logger.Logf(msg) + + return nil, err + } + + fileinfo := make([]file.FileInfo, 0, len(prefixes)+len(objects)) + + for _, p := range prefixes { + trimmedName := strings.TrimSuffix(p, "/") + dirName := path.Base(trimmedName) + fileinfo = append(fileinfo, &File{ + name: dirName, + isDir: true, + logger: f.logger, + metrics: f.metrics, + }) + } + + for _, o := range objects { + fileinfo = append(fileinfo, &File{ + name: path.Base(o.Name), + size: o.Size, + lastModified: o.Updated, + isDir: false, + logger: f.logger, + metrics: f.metrics, + }) + } + + st = statusSuccess + msg = fmt.Sprintf("Directory/Files in directory with path %q retrieved successfully", dir) + + f.logger.Logf("Reading directory/files from GCS at path %q successful.", dir) + + return fileinfo, nil +} + +func (f *FileSystem) ChDir(_ string) error { + const op = "CHDIR" + + st := statusErr + + var msg = "Changing directory not supported" + + defer f.sendOperationStats(&FileLog{ + Operation: op, + Location: getLocation(f.config.BucketName), + Status: &st, + Message: &msg, + }, time.Now()) + + f.logger.Errorf("%s: not supported in GCS", op) + + return errCHNDIRNotSupported +} +func (f *FileSystem) Getwd() (string, error) { + const op = "GETWD" + + st := statusSuccess + + start := time.Now() + + var msg = "Returning simulated root directory" + + defer f.sendOperationStats(&FileLog{ + Operation: op, + Location: getLocation(f.config.BucketName), + Status: &st, + Message: &msg, + }, start) + + return getLocation(f.config.BucketName), nil +} +func (f *FileSystem) Stat(name string) (file.FileInfo, error) { + var msg string + + st := statusErr + + defer f.sendOperationStats(&FileLog{ + Operation: "STAT", + Location: getLocation(f.config.BucketName), + Status: &st, + Message: &msg, + }, time.Now()) + + ctx := context.TODO() + + // Try to stat the object (file) + attr, err := f.conn.StatObject(ctx, name) + if err == nil { + st = statusSuccess + msg = fmt.Sprintf("File with path %q info retrieved successfully", name) + + return &File{ + name: name, + logger: f.logger, + metrics: f.metrics, + size: attr.Size, + contentType: attr.ContentType, + lastModified: attr.Updated, + }, nil + } + + // If not found, check if it's a "directory" by listing with prefix + if errors.Is(err, storage.ErrObjectNotExist) { + // Ensure the name ends with slash for directories + prefix := name + if !strings.HasSuffix(prefix, "/") { + prefix += "/" + } + + objs, _, listErr := f.conn.ListDir(ctx, prefix) + + if listErr != nil { + f.logger.Errorf("Error checking directory prefix: %v", listErr) + return nil, listErr + } + + if len(objs) > 0 { + st = statusSuccess + msg = fmt.Sprintf("Directory with path %q info retrieved successfully", name) + + return &File{ + name: name, + logger: f.logger, + metrics: f.metrics, + size: 0, + contentType: "application/x-directory", + lastModified: objs[0].Updated, + }, nil + } + } + + f.logger.Errorf("Error returning file or directory info: %v", err) + + return nil, err +} + +func (f *FileSystem) sendOperationStats(fl *FileLog, startTime time.Time) { + duration := time.Since(startTime).Microseconds() + + fl.Duration = duration + + f.logger.Debug(fl) +} diff --git a/pkg/gofr/datasource/file/gcs/fs_dir_test.go b/pkg/gofr/datasource/file/gcs/fs_dir_test.go new file mode 100644 index 000000000..c36011d0b --- /dev/null +++ b/pkg/gofr/datasource/file/gcs/fs_dir_test.go @@ -0,0 +1,236 @@ +package gcs + +import ( + "bytes" + "errors" + "testing" + + "cloud.google.com/go/storage" + "github.com/golang/mock/gomock" + "github.com/stretchr/testify/require" +) + +type fakeWriteCloser struct { + *bytes.Buffer +} + +func (*fakeWriteCloser) Close() error { + return nil +} + +type errorWriterCloser struct{} + +var ( + errWrite = errors.New("write error") + errClose = errors.New("close error") + errDirNotFound = errors.New("directory not found") +) + +func (*errorWriterCloser) Write(_ []byte) (int, error) { + return 0, errWrite +} + +func (*errorWriterCloser) Close() error { + return errClose +} + +type result struct { + Name string + Size int64 + IsDir bool +} + +func Test_Mkdir_GCS(t *testing.T) { + type testCase struct { + name string + dirName string + setupMocks func(mockGCS *MockgcsClient) + expectError bool + } + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockGCS := NewMockgcsClient(ctrl) + mockLogger := NewMockLogger(ctrl) + mockMetrics := NewMockMetrics(ctrl) + + config := &Config{ + BucketName: "test-bucket", + CredentialsJSON: "fake-creds", + ProjectID: "test-project", + } + + fs := &FileSystem{ + conn: mockGCS, + config: config, + logger: mockLogger, + metrics: mockMetrics, + } + + mockLogger.EXPECT().Logf(gomock.Any(), gomock.Any()).AnyTimes() + mockLogger.EXPECT().Debugf(gomock.Any()).AnyTimes() + mockLogger.EXPECT().Debug(gomock.Any()).AnyTimes() + mockLogger.EXPECT().Errorf(gomock.Any(), gomock.Any()).AnyTimes() + mockMetrics.EXPECT().RecordHistogram(gomock.Any(), appFTPStats, gomock.Any(), + "type", gomock.Any(), "status", gomock.Any()).AnyTimes() + + tests := []testCase{ + { + name: "successfully create directory", + dirName: "testDir", + setupMocks: func(m *MockgcsClient) { + buf := &bytes.Buffer{} + fakeWriter := &fakeWriteCloser{Buffer: buf} + m.EXPECT().NewWriter(gomock.Any(), "testDir/").Return(fakeWriter) + }, + expectError: false, + }, + { + name: "fail when directory name is empty", + dirName: "", + setupMocks: func(_ *MockgcsClient) { + // No mock needed for empty dir + }, + expectError: true, + }, + { + name: "fail when GCS write fails", + dirName: "brokenDir", + setupMocks: func(m *MockgcsClient) { + errorWriter := &errorWriterCloser{} + m.EXPECT().NewWriter(gomock.Any(), "brokenDir/").Return(errorWriter) + }, + expectError: true, + }, + } + + for i, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.setupMocks(mockGCS) + + err := fs.Mkdir(tt.dirName, 0777) + + if tt.expectError { + require.Error(t, err, "Test %d (%s): expected an error", i, tt.name) + } else { + require.NoError(t, err, "Test %d (%s): expected no error", i, tt.name) + } + }) + } +} + +func Test_ReadDir_GCS(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockGCS := NewMockgcsClient(ctrl) + mockLogger := NewMockLogger(ctrl) + mockMetrics := NewMockMetrics(ctrl) + + fs := &FileSystem{ + conn: mockGCS, + config: &Config{BucketName: "test-bucket"}, + logger: mockLogger, + metrics: mockMetrics, + } + + mockLogger.EXPECT().Logf(gomock.Any(), gomock.Any()).AnyTimes() + mockLogger.EXPECT().Debug(gomock.Any()).AnyTimes() + mockLogger.EXPECT().Errorf(gomock.Any(), gomock.Any()).AnyTimes() + mockMetrics.EXPECT().RecordHistogram(gomock.Any(), appFTPStats, gomock.Any(), + "type", gomock.Any(), "status", gomock.Any()).AnyTimes() + + for _, tt := range getReadDirTestCases(mockGCS) { + t.Run(tt.name, func(t *testing.T) { + tt.setupMock() + entries, err := fs.ReadDir(tt.dirPath) + + if tt.expectError { + require.Error(t, err) + return + } + + require.NoError(t, err) + require.Len(t, entries, len(tt.expectedResults)) + + for i, entry := range entries { + require.Equal(t, tt.expectedResults[i].Name, entry.Name()) + require.Equal(t, tt.expectedResults[i].IsDir, entry.IsDir()) + } + }) + } +} + +type readDirTestCase struct { + name string + dirPath string + expectedResults []result + setupMock func() + expectError bool +} + +func getReadDirTestCases(mockGCS *MockgcsClient) []readDirTestCase { + return []readDirTestCase{ + { + name: "Valid directory path with files and subdirectory", + dirPath: "abc/efg", + expectedResults: []result{ + {"hij", 0, true}, + {"file.txt", 1, false}, + }, + setupMock: func() { + mockGCS.EXPECT().ListDir(gomock.Any(), "abc/efg").Return( + []*storage.ObjectAttrs{{Name: "abc/efg/file.txt", Size: 1}}, + []string{"abc/efg/hij/"}, + nil, + ) + }, + }, + { + name: "Valid directory path with only subdirectory", + dirPath: "abc", + expectedResults: []result{ + {"efg", 0, true}, + }, + setupMock: func() { + mockGCS.EXPECT().ListDir(gomock.Any(), "abc").Return( + []*storage.ObjectAttrs{}, + []string{"abc/efg/"}, + nil, + ) + }, + }, + { + name: "Directory not found", + dirPath: "does-not-exist", + expectedResults: nil, + setupMock: func() { + mockGCS.EXPECT().ListDir(gomock.Any(), "does-not-exist").Return(nil, nil, errDirNotFound) + }, + expectError: true, + }, + { + name: "Empty directory", + dirPath: "empty", + expectedResults: []result{}, + setupMock: func() { + mockGCS.EXPECT().ListDir(gomock.Any(), "empty").Return([]*storage.ObjectAttrs{}, nil, nil) + }, + }, + { + name: "Directory with multiple files", + dirPath: "many/files", + expectedResults: []result{ + {"file1.txt", 1, false}, + {"file2.txt", 2, false}, + }, + setupMock: func() { + mockGCS.EXPECT().ListDir(gomock.Any(), "many/files").Return([]*storage.ObjectAttrs{ + {Name: "many/files/file1.txt", Size: 1}, + {Name: "many/files/file2.txt", Size: 2}, + }, nil, nil) + }, + }, + } +} diff --git a/pkg/gofr/datasource/file/gcs/fs_test.go b/pkg/gofr/datasource/file/gcs/fs_test.go new file mode 100644 index 000000000..e57b69e3c --- /dev/null +++ b/pkg/gofr/datasource/file/gcs/fs_test.go @@ -0,0 +1,374 @@ +package gcs + +import ( + "errors" + "fmt" + "testing" + "time" + + "cloud.google.com/go/storage" + "github.com/golang/mock/gomock" + "github.com/stretchr/testify/require" + "gotest.tools/v3/assert" +) + +var ( + errObjectNotFound = errors.New("object not found") + errMock = fmt.Errorf("errMock") +) + +func Test_CreateFile(t *testing.T) { + type testCase struct { + name string + createPath string + setupMocks func(mockGCS *MockgcsClient) + expectError bool + isRoot bool + } + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockGCS := NewMockgcsClient(ctrl) + mockLogger := NewMockLogger(ctrl) + mockMetrics := NewMockMetrics(ctrl) + + config := &Config{ + BucketName: "test-bucket", + CredentialsJSON: "fake-creds", + ProjectID: "test-project", + } + + fs := &FileSystem{ + conn: mockGCS, + config: config, + logger: mockLogger, + metrics: mockMetrics, + } + + mockLogger.EXPECT().Logf(gomock.Any(), gomock.Any()).AnyTimes() + mockLogger.EXPECT().Errorf(gomock.Any(), gomock.Any()).AnyTimes() + mockLogger.EXPECT().Debug(gomock.Any()).AnyTimes() + mockMetrics.EXPECT().RecordHistogram(gomock.Any(), appFTPStats, gomock.Any(), + "type", gomock.Any(), "status", gomock.Any()).AnyTimes() + + tests := []testCase{ + { + name: "create file at root level", + createPath: "abc.txt", + setupMocks: func(m *MockgcsClient) { + m.EXPECT().ListObjects(gomock.Any(), ".").Return([]string{}, nil) + m.EXPECT().ListObjects(gomock.Any(), "abc.txt").Return([]string{}, nil) + m.EXPECT().NewWriter(gomock.Any(), "abc.txt").Return(&storage.Writer{}) + }, + + expectError: false, + isRoot: true, + }, + { + name: "fail when parent directory does not exist", + createPath: "abc/abc.txt", + setupMocks: func(m *MockgcsClient) { + m.EXPECT().ListObjects(gomock.Any(), "abc/").Return(nil, errMock) + }, + expectError: true, + isRoot: false, + }, + { + name: "create file inside existing directory", + createPath: "abc/efg.txt", + setupMocks: func(m *MockgcsClient) { + // parent path "abc/" exists + m.EXPECT().ListObjects(gomock.Any(), "abc/").Return([]string{"abc/.keep"}, nil) + // filename does not exist + m.EXPECT().ListObjects(gomock.Any(), "abc/efg.txt").Return([]string{}, nil) + m.EXPECT().NewWriter(gomock.Any(), "abc/efg.txt").Return(&storage.Writer{}) + }, + expectError: false, + isRoot: false, + }, + } + + for i, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.setupMocks(mockGCS) + + file, err := fs.Create(tt.createPath) + + if tt.expectError { + require.Error(t, err, "Test %d (%s): expected an error", i, tt.name) + return + } + + require.NoError(t, err, "Test %d (%s): expected no error", i, tt.name) + require.NotNil(t, file) + }) + } +} +func Test_Remove_GCS(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockGCS := NewMockgcsClient(ctrl) + mockLogger := NewMockLogger(ctrl) + mockMetrics := NewMockMetrics(ctrl) + + fs := &FileSystem{ + conn: mockGCS, + logger: mockLogger, + config: &Config{BucketName: "test-bucket"}, + metrics: mockMetrics, + } + + // Expectations + mockLogger.EXPECT().Logf(gomock.Any(), gomock.Any()).AnyTimes() + mockLogger.EXPECT().Errorf(gomock.Any(), gomock.Any()).AnyTimes() + mockLogger.EXPECT().Debug(gomock.Any()).AnyTimes() + mockMetrics.EXPECT().RecordHistogram(gomock.Any(), appFTPStats, gomock.Any(), + "type", gomock.Any(), "status", gomock.Any()).AnyTimes() + + mockGCS.EXPECT(). + DeleteObject(gomock.Any(), "abc/a1.txt"). + Return(nil). + Times(1) + + err := fs.Remove("abc/a1.txt") + require.NoError(t, err) +} + +var ( + errDeleteFailed = errors.New("delete failed") + errCopyFailed = errors.New("copy failed") +) + +func TestRenameFile(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockConn := NewMockgcsClient(ctrl) + mockLogger := NewMockLogger(ctrl) + mockMetrics := NewMockMetrics(ctrl) + + config := &Config{BucketName: "test-bucket"} + + fs := &FileSystem{ + conn: mockConn, + config: config, + logger: mockLogger, + metrics: mockMetrics, + } + + tests := []struct { + name string + initialName string + newName string + setupMocks func() + expectedError bool + }{ + { + name: "Rename file to new name", + initialName: "dir/file.txt", + newName: "dir/file-renamed.txt", + setupMocks: func() { + mockConn.EXPECT().CopyObject(gomock.Any(), "dir/file.txt", "dir/file-renamed.txt").Return(nil) + mockConn.EXPECT().DeleteObject(gomock.Any(), "dir/file.txt").Return(nil) + }, + expectedError: false, + }, + { + name: "Rename file with copy failure", + initialName: "dir/file.txt", + newName: "dir/file-renamed.txt", + setupMocks: func() { + mockConn.EXPECT().CopyObject(gomock.Any(), "dir/file.txt", "dir/file-renamed.txt").Return(errCopyFailed) + }, + expectedError: true, + }, + { + name: "Rename file with delete failure", + initialName: "dir/file.txt", + newName: "dir/file-renamed.txt", + setupMocks: func() { + mockConn.EXPECT().CopyObject(gomock.Any(), "dir/file.txt", "dir/file-renamed.txt").Return(nil) + mockConn.EXPECT().DeleteObject(gomock.Any(), "dir/file.txt").Return(errDeleteFailed) + }, + expectedError: true, + }, + { + name: "Rename file to same name", + initialName: "dir/file.txt", + newName: "dir/file.txt", + setupMocks: func() {}, // No calls expected + expectedError: false, + }, + { + name: "Rename file to different directory (not allowed)", + initialName: "dir1/file.txt", + newName: "dir2/file.txt", + setupMocks: func() {}, // No calls expected + expectedError: true, + }, + } + + // Set up logger mocks globally + mockLogger.EXPECT().Logf(gomock.Any(), gomock.Any()).AnyTimes() + mockLogger.EXPECT().Debug(gomock.Any()).AnyTimes() + mockLogger.EXPECT().Errorf(gomock.Any(), gomock.Any()).AnyTimes() + mockLogger.EXPECT().Debug(gomock.Any(), gomock.Any()).AnyTimes() + mockMetrics.EXPECT().RecordHistogram(gomock.Any(), appFTPStats, gomock.Any(), + "type", gomock.Any(), "status", gomock.Any()).AnyTimes() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.setupMocks() + err := fs.Rename(tt.initialName, tt.newName) + + if tt.expectedError { + require.Error(t, err, "Expected error but got none") + } else { + require.NoError(t, err, "Unexpected error: %v", err) + } + }) + } +} + +func Test_StatFile_GCS(t *testing.T) { + tm := time.Now() + + type result struct { + name string + size int64 + isDir bool + } + + tests := []struct { + name string + filePath string + mockAttr *storage.ObjectAttrs + mockError error + expected result + expectError bool + }{ + { + name: "Valid file stat", + filePath: "abc/efg/file.txt", + mockAttr: &storage.ObjectAttrs{ + Name: "abc/efg/file.txt", + Size: 123, + Updated: tm, + ContentType: "text/plain", + }, + expected: result{ + name: "abc/efg/file.txt", + size: 123, + isDir: false, + }, + }, + { + name: "File not found", + filePath: "notfound.txt", + mockAttr: nil, + mockError: errObjectNotFound, + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockGCS := NewMockgcsClient(ctrl) + mockLogger := NewMockLogger(ctrl) + mockMetrics := NewMockMetrics(ctrl) + + config := &Config{BucketName: "test-bucket"} + + fs := &FileSystem{ + conn: mockGCS, + config: config, + logger: mockLogger, + metrics: mockMetrics, + } + + mockLogger.EXPECT().Logf(gomock.Any(), gomock.Any()).AnyTimes() + mockLogger.EXPECT().Debug(gomock.Any()).AnyTimes() + mockLogger.EXPECT().Errorf(gomock.Any(), gomock.Any()).AnyTimes() + + mockGCS.EXPECT().StatObject(gomock.Any(), tt.filePath).Return(tt.mockAttr, tt.mockError) + mockMetrics.EXPECT().RecordHistogram(gomock.Any(), appFTPStats, gomock.Any(), + "type", gomock.Any(), "status", gomock.Any()).AnyTimes() + + res, err := fs.Stat(tt.filePath) + if tt.expectError { + require.Error(t, err) + return + } + + require.NoError(t, err) + + actual := result{ + name: res.Name(), + size: res.Size(), + isDir: res.IsDir(), + } + + assert.Equal(t, tt.expected, actual) + }) + } +} +func Test_Stat_FileAndDir(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockGCS := NewMockgcsClient(ctrl) + mockLogger := NewMockLogger(ctrl) + mockMetrics := NewMockMetrics(ctrl) + + fs := &FileSystem{ + conn: mockGCS, + logger: mockLogger, + metrics: mockMetrics, + config: &Config{ + BucketName: "test-bucket", + }, + } + + mockLogger.EXPECT().Logf(gomock.Any(), gomock.Any()).AnyTimes() + mockLogger.EXPECT().Debug(gomock.Any()).AnyTimes() + mockLogger.EXPECT().Errorf(gomock.Any(), gomock.Any()).AnyTimes() + mockMetrics.EXPECT().RecordHistogram(gomock.Any(), appFTPStats, gomock.Any(), + "type", gomock.Any(), "status", gomock.Any()).AnyTimes() + + fileName := "documents/testfile.txt" + fileAttrs := &storage.ObjectAttrs{ + Name: fileName, + Size: 1024, + ContentType: "text/plain", + Updated: time.Now(), + } + mockGCS.EXPECT().StatObject(gomock.Any(), fileName).Return(fileAttrs, nil) + + info, err := fs.Stat(fileName) + assert.NilError(t, err) + assert.Equal(t, fileName, info.Name()) + assert.Equal(t, int64(1024), info.Size()) + assert.Check(t, !info.IsDir()) + + dirName := "documents/folder/" + dirAttrs := &storage.ObjectAttrs{ + Name: dirName, + Size: 0, + ContentType: "application/x-directory", + Updated: time.Now(), + } + + mockGCS.EXPECT().StatObject(gomock.Any(), dirName).Return(dirAttrs, nil) + + info, err = fs.Stat(dirName) + + assert.NilError(t, err) + assert.Equal(t, dirName, info.Name()) + assert.Equal(t, int64(0), info.Size()) + assert.Check(t, info.IsDir()) +} diff --git a/pkg/gofr/datasource/file/gcs/go.mod b/pkg/gofr/datasource/file/gcs/go.mod new file mode 100644 index 000000000..705879dd3 --- /dev/null +++ b/pkg/gofr/datasource/file/gcs/go.mod @@ -0,0 +1,68 @@ +module gofr.dev/pkg/gofr/datasource/file/gcs + +go 1.25.0 + +require ( + cloud.google.com/go/storage v1.55.0 + github.com/golang/mock v1.6.0 + github.com/stretchr/testify v1.10.0 + gofr.dev v1.42.2 + google.golang.org/api v0.238.0 + gotest.tools/v3 v3.5.2 +) + +require ( + cel.dev/expr v0.23.0 // indirect + cloud.google.com/go v0.121.1 // indirect + cloud.google.com/go/auth v0.16.2 // indirect + cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect + cloud.google.com/go/compute/metadata v0.7.0 // indirect + cloud.google.com/go/iam v1.5.2 // indirect + cloud.google.com/go/monitoring v1.24.2 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.51.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/cncf/xds/go v0.0.0-20250326154945-ae57f3c0d45f // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/envoyproxy/go-control-plane/envoy v1.32.4 // indirect + github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/go-jose/go-jose/v4 v4.0.5 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/google/go-cmp v0.7.0 // indirect + github.com/google/s2a-go v0.1.9 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect + github.com/googleapis/gax-go/v2 v2.14.2 // indirect + github.com/joho/godotenv v1.5.1 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/spiffe/go-spiffe/v2 v2.5.0 // indirect + github.com/zeebo/errs v1.4.0 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/contrib/detectors/gcp v1.36.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0 // indirect + go.opentelemetry.io/otel v1.37.0 // indirect + go.opentelemetry.io/otel/metric v1.37.0 // indirect + go.opentelemetry.io/otel/sdk v1.37.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.37.0 // indirect + go.opentelemetry.io/otel/trace v1.37.0 // indirect + go.uber.org/mock v0.5.2 // indirect + golang.org/x/crypto v0.39.0 // indirect + golang.org/x/net v0.41.0 // indirect + golang.org/x/oauth2 v0.30.0 // indirect + golang.org/x/sync v0.15.0 // indirect + golang.org/x/sys v0.33.0 // indirect + golang.org/x/text v0.26.0 // indirect + golang.org/x/time v0.12.0 // indirect + google.golang.org/genproto v0.0.0-20250505200425-f936aa4a68b2 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect + google.golang.org/grpc v1.73.0 // indirect + google.golang.org/protobuf v1.36.6 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/pkg/gofr/datasource/file/gcs/go.sum b/pkg/gofr/datasource/file/gcs/go.sum new file mode 100644 index 000000000..9ecffed09 --- /dev/null +++ b/pkg/gofr/datasource/file/gcs/go.sum @@ -0,0 +1,171 @@ +cel.dev/expr v0.23.0 h1:wUb94w6OYQS4uXraxo9U+wUAs9jT47Xvl4iPgAwM2ss= +cel.dev/expr v0.23.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= +cloud.google.com/go v0.121.1 h1:S3kTQSydxmu1JfLRLpKtxRPA7rSrYPRPEUmL/PavVUw= +cloud.google.com/go v0.121.1/go.mod h1:nRFlrHq39MNVWu+zESP2PosMWA0ryJw8KUBZ2iZpxbw= +cloud.google.com/go/auth v0.16.2 h1:QvBAGFPLrDeoiNjyfVunhQ10HKNYuOwZ5noee0M5df4= +cloud.google.com/go/auth v0.16.2/go.mod h1:sRBas2Y1fB1vZTdurouM0AzuYQBMZinrUYL8EufhtEA= +cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= +cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= +cloud.google.com/go/compute/metadata v0.7.0 h1:PBWF+iiAerVNe8UCHxdOt6eHLVc3ydFeOCw78U8ytSU= +cloud.google.com/go/compute/metadata v0.7.0/go.mod h1:j5MvL9PprKL39t166CoB1uVHfQMs4tFQZZcKwksXUjo= +cloud.google.com/go/iam v1.5.2 h1:qgFRAGEmd8z6dJ/qyEchAuL9jpswyODjA2lS+w234g8= +cloud.google.com/go/iam v1.5.2/go.mod h1:SE1vg0N81zQqLzQEwxL2WI6yhetBdbNQuTvIKCSkUHE= +cloud.google.com/go/logging v1.13.0 h1:7j0HgAp0B94o1YRDqiqm26w4q1rDMH7XNRU34lJXHYc= +cloud.google.com/go/logging v1.13.0/go.mod h1:36CoKh6KA/M0PbhPKMq6/qety2DCAErbhXT62TuXALA= +cloud.google.com/go/longrunning v0.6.7 h1:IGtfDWHhQCgCjwQjV9iiLnUta9LBCo8R9QmAFsS/PrE= +cloud.google.com/go/longrunning v0.6.7/go.mod h1:EAFV3IZAKmM56TyiE6VAP3VoTzhZzySwI/YI1s/nRsY= +cloud.google.com/go/monitoring v1.24.2 h1:5OTsoJ1dXYIiMiuL+sYscLc9BumrL3CarVLL7dd7lHM= +cloud.google.com/go/monitoring v1.24.2/go.mod h1:x7yzPWcgDRnPEv3sI+jJGBkwl5qINf+6qY4eq0I9B4U= +cloud.google.com/go/storage v1.55.0 h1:NESjdAToN9u1tmhVqhXCaCwYBuvEhZLLv0gBr+2znf0= +cloud.google.com/go/storage v1.55.0/go.mod h1:ztSmTTwzsdXe5syLVS0YsbFxXuvEmEyZj7v7zChEmuY= +cloud.google.com/go/trace v1.11.6 h1:2O2zjPzqPYAHrn3OKl029qlqG6W8ZdYaOWRyr8NgMT4= +cloud.google.com/go/trace v1.11.6/go.mod h1:GA855OeDEBiBMzcckLPE2kDunIpC72N+Pq8WFieFjnI= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0 h1:ErKg/3iS1AKcTkf3yixlZ54f9U1rljCkQyEXWUnIUxc= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0/go.mod h1:yAZHSGnqScoU556rBOVkwLze6WP5N+U11RHuWaGVxwY= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0 h1:fYE9p3esPxA/C0rQ0AHhP0drtPXDRhaWiwg1DPqO7IU= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0/go.mod h1:BnBReJLvVYx2CS/UHOgVz2BXKXD9wsQPxZug20nZhd0= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.51.0 h1:OqVGm6Ei3x5+yZmSJG1Mh2NwHvpVmZ08CB5qJhT9Nuk= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.51.0/go.mod h1:SZiPHWGOOk3bl8tkevxkoiwPgsIl6CwrWcbwjfHZpdM= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.51.0 h1:6/0iUd0xrnX7qt+mLNRwg5c0PGv8wpE8K90ryANQwMI= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.51.0/go.mod h1:otE2jQekW/PqXk1Awf5lmfokJx4uwuqcj1ab5SpGeW0= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cncf/xds/go v0.0.0-20250326154945-ae57f3c0d45f h1:C5bqEmzEPLsHm9Mv73lSE9e9bKV23aB1vxOsmZrkl3k= +github.com/cncf/xds/go v0.0.0-20250326154945-ae57f3c0d45f/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= +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/envoyproxy/go-control-plane v0.13.4 h1:zEqyPVyku6IvWCFwux4x9RxkLOMUL+1vC9xUFv5l2/M= +github.com/envoyproxy/go-control-plane v0.13.4/go.mod h1:kDfuBlDVsSj2MjrLEtRWtHlsWIFcGyB2RMO44Dc5GZA= +github.com/envoyproxy/go-control-plane/envoy v1.32.4 h1:jb83lalDRZSpPWW2Z7Mck/8kXZ5CQAFYVjQcdVIr83A= +github.com/envoyproxy/go-control-plane/envoy v1.32.4/go.mod h1:Gzjc5k8JcJswLjAx1Zm+wSYE20UrLtt7JZMWiWQXQEw= +github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI= +github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4= +github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8= +github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE= +github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/martian/v3 v3.3.3 h1:DIhPTQrbPkgs2yJYdXU/eNACCG5DVQjySNRNlflZ9Fc= +github.com/google/martian/v3 v3.3.3/go.mod h1:iEPrYcgCF7jA9OtScMFQyAlZZ4YXTKEtJ1E6RWzmBA0= +github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= +github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4= +github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= +github.com/googleapis/gax-go/v2 v2.14.2 h1:eBLnkZ9635krYIPD+ag1USrOAI0Nr0QYF3+/3GqO0k0= +github.com/googleapis/gax-go/v2 v2.14.2/go.mod h1:ON64QhlJkhVtSqp4v1uaK92VyZ2gmvDQsweuyLV+8+w= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= +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/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/spiffe/go-spiffe/v2 v2.5.0 h1:N2I01KCUkv1FAjZXJMwh95KK1ZIQLYbPfhaxw8WS0hE= +github.com/spiffe/go-spiffe/v2 v2.5.0/go.mod h1:P+NxobPc6wXhVtINNtFjNWGBTreew1GBUCwT2wPmb7g= +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/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/zeebo/errs v1.4.0 h1:XNdoD/RRMKP7HD0UhJnIzUy74ISdGGxURlYG8HSWSfM= +github.com/zeebo/errs v1.4.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/contrib/detectors/gcp v1.36.0 h1:F7q2tNlCaHY9nMKHR6XH9/qkp8FktLnIcy6jJNyOCQw= +go.opentelemetry.io/contrib/detectors/gcp v1.36.0/go.mod h1:IbBN8uAIIx734PTonTPxAxnjc2pQTxWNkwfstZ+6H2k= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 h1:q4XOmH/0opmeuJtPsbFNivyl7bCt7yRBbeEm2sC/XtQ= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0 h1:Hf9xI/XLML9ElpiHVDNwvqI0hIFlzV8dgIr35kV1kRU= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0/go.mod h1:NfchwuyNoMcZ5MLHwPrODwUF1HWCXWrL31s8gSAdIKY= +go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= +go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.36.0 h1:rixTyDGXFxRy1xzhKrotaHy3/KXdPhlWARrCgK+eqUY= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.36.0/go.mod h1:dowW6UsM9MKbJq5JTz2AMVp3/5iW5I/TStsk8S+CfHw= +go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= +go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= +go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI= +go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg= +go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc= +go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps= +go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= +go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= +go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko= +go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o= +gofr.dev v1.42.2 h1:cqWFwnYzaDyNZgMDxdwgb4Wk3/1kam0E+mpXOyeiU4Q= +gofr.dev v1.42.2/go.mod h1:viVap8+T4Uk6FbeK2brW3U8dau4R8xiGoo+/NyY7zrE= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= +golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= +golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= +golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= +golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= +golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= +golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= +golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= +golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= +golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.238.0 h1:+EldkglWIg/pWjkq97sd+XxH7PxakNYoe/rkSTbnvOs= +google.golang.org/api v0.238.0/go.mod h1:cOVEm2TpdAGHL2z+UwyS+kmlGr3bVWQQ6sYEqkKje50= +google.golang.org/genproto v0.0.0-20250505200425-f936aa4a68b2 h1:1tXaIXCracvtsRxSBsYDiSBN0cuJvM7QYW+MrpIRY78= +google.golang.org/genproto v0.0.0-20250505200425-f936aa4a68b2/go.mod h1:49MsLSx0oWMOZqcpB3uL8ZOkAh1+TndpJ8ONoCBWiZk= +google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 h1:oWVWY3NzT7KJppx2UKhKmzPq4SRe0LdCijVRwvGeikY= +google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822/go.mod h1:h3c4v36UTKzUiuaOKQ6gr3S+0hovBtUrXzTG/i3+XEc= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= +google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok= +google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc= +google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= +google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= +gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= diff --git a/pkg/gofr/datasource/file/gcs/interface.go b/pkg/gofr/datasource/file/gcs/interface.go new file mode 100644 index 000000000..2d2297eb7 --- /dev/null +++ b/pkg/gofr/datasource/file/gcs/interface.go @@ -0,0 +1,123 @@ +//go:generate mockgen -source=interface.go -destination=mock_interface.go -package=gcs + +package gcs + +import ( + "context" + "errors" + "fmt" + "io" + + "cloud.google.com/go/storage" + "google.golang.org/api/iterator" +) + +type Logger interface { + Debug(args ...any) + Debugf(pattern string, args ...any) + Logf(pattern string, args ...any) + Errorf(pattern string, args ...any) +} +type gcsClientImpl struct { + client *storage.Client + bucket *storage.BucketHandle +} + +type gcsClient interface { + NewWriter(ctx context.Context, name string) io.WriteCloser + NewReader(ctx context.Context, name string) (*storage.Reader, error) + DeleteObject(ctx context.Context, name string) error + CopyObject(ctx context.Context, src, dst string) error + ListObjects(ctx context.Context, prefix string) ([]string, error) + ListDir(ctx context.Context, prefix string) ([]*storage.ObjectAttrs, []string, error) + StatObject(ctx context.Context, name string) (*storage.ObjectAttrs, error) +} + +type Metrics interface { + NewHistogram(name, desc string, buckets ...float64) + RecordHistogram(ctx context.Context, name string, value float64, labels ...string) +} + +func (g *gcsClientImpl) NewWriter(ctx context.Context, name string) io.WriteCloser { + return g.bucket.Object(name).NewWriter(ctx) +} + +func (g *gcsClientImpl) NewReader(ctx context.Context, name string) (*storage.Reader, error) { + return g.bucket.Object(name).NewReader(ctx) +} + +func (g *gcsClientImpl) DeleteObject(ctx context.Context, name string) error { + attrs, err := g.bucket.Object(name).Attrs(ctx) + if err != nil { + return fmt.Errorf("failed to get object attributes: %w", err) + } + + err = g.bucket.Object(name).If(storage.Conditions{GenerationMatch: attrs.Generation}).Delete(ctx) + if err != nil { + return fmt.Errorf("failed to delete object: %w", err) + } + + return nil +} + +func (g *gcsClientImpl) CopyObject(ctx context.Context, src, dst string) error { + srcObj := g.bucket.Object(src) + dstObj := g.bucket.Object(dst) + _, err := dstObj.CopierFrom(srcObj).Run(ctx) + + return err +} + +func (g *gcsClientImpl) ListObjects(ctx context.Context, prefix string) ([]string, error) { + var objects []string + + it := g.bucket.Objects(ctx, &storage.Query{Prefix: prefix}) + + for { + obj, err := it.Next() + if errors.Is(err, iterator.Done) { + break + } + + if err != nil { + return nil, err + } + + objects = append(objects, obj.Name) + } + + return objects, nil +} + +func (g *gcsClientImpl) ListDir(ctx context.Context, prefix string) ([]*storage.ObjectAttrs, []string, error) { + var attrs []*storage.ObjectAttrs + + var prefixes []string + + it := g.bucket.Objects(ctx, &storage.Query{ + Prefix: prefix, + Delimiter: "/", + }) + + for { + obj, err := it.Next() + + if errors.Is(err, iterator.Done) { + break + } else if err != nil { + return nil, nil, err + } + + if obj.Prefix != "" { + prefixes = append(prefixes, obj.Prefix) + } else { + attrs = append(attrs, obj) + } + } + + return attrs, prefixes, nil +} + +func (g *gcsClientImpl) StatObject(ctx context.Context, name string) (*storage.ObjectAttrs, error) { + return g.bucket.Object(name).Attrs(ctx) +} diff --git a/pkg/gofr/datasource/file/gcs/logger.go b/pkg/gofr/datasource/file/gcs/logger.go new file mode 100644 index 000000000..992b79b29 --- /dev/null +++ b/pkg/gofr/datasource/file/gcs/logger.go @@ -0,0 +1,34 @@ +package gcs + +import ( + "fmt" + "io" + "regexp" + "strings" +) + +// FileLog handles logging with different levels. +// In DEBUG MODE, this FileLog can be exported into a file while +// running the application or can be logged in the terminal. +type FileLog struct { + Operation string `json:"operation"` + Duration int64 `json:"duration"` + Status *string `json:"status"` + Location string `json:"location,omitempty"` + Message *string `json:"message,omitempty"` +} + +var regexpSpaces = regexp.MustCompile(`\s+`) + +func clean(query *string) string { + if query == nil { + return "" + } + + return strings.TrimSpace(regexpSpaces.ReplaceAllString(*query, " ")) +} + +func (fl *FileLog) PrettyPrint(writer io.Writer) { + fmt.Fprintf(writer, "\u001B[38;5;8m%-32s \u001B[38;5;148m%-6s\u001B[0m %8d\u001B[38;5;8mµs\u001B[0m %-10s \u001B[0m %-48s \n", + clean(&fl.Operation), "GCS", fl.Duration, clean(fl.Status), clean(fl.Message)) +} diff --git a/pkg/gofr/datasource/file/gcs/logger_test.go b/pkg/gofr/datasource/file/gcs/logger_test.go new file mode 100644 index 000000000..a6e156d6d --- /dev/null +++ b/pkg/gofr/datasource/file/gcs/logger_test.go @@ -0,0 +1,48 @@ +package gcs + +import ( + "bytes" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestFileLogPrettyPrint(t *testing.T) { + msg := "File Created successfully" + + fileLog := FileLog{ + Operation: "Create file", + Duration: 1234, + Location: "/ftp/one", + Message: &msg, + } + + expected := "Create file" + + expectedMsg := "File Created successfully" + + var buf bytes.Buffer + + fileLog.PrettyPrint(&buf) + + assert.Contains(t, buf.String(), expected) + assert.Contains(t, buf.String(), expectedMsg) +} + +func TestFileLogPrettyPrintWhitespaceHandling(t *testing.T) { + msg := " File creation complete " + fileLog := FileLog{ + Operation: " Create file ", + Duration: 5678, + Message: &msg, + } + expected := "Create file" + expectedMsg := "File creation complete" + + var buf bytes.Buffer + + fileLog.PrettyPrint(&buf) + + assert.Contains(t, buf.String(), expected) + assert.Contains(t, buf.String(), expectedMsg) +} diff --git a/pkg/gofr/datasource/file/gcs/mock_interface.go b/pkg/gofr/datasource/file/gcs/mock_interface.go new file mode 100644 index 000000000..31d3ba436 --- /dev/null +++ b/pkg/gofr/datasource/file/gcs/mock_interface.go @@ -0,0 +1,287 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: interface.go + +// Package gcs is a generated GoMock package. +package gcs + +import ( + context "context" + io "io" + reflect "reflect" + + storage "cloud.google.com/go/storage" + gomock "github.com/golang/mock/gomock" +) + +// MockLogger is a mock of Logger interface. +type MockLogger struct { + ctrl *gomock.Controller + recorder *MockLoggerMockRecorder +} + +// MockLoggerMockRecorder is the mock recorder for MockLogger. +type MockLoggerMockRecorder struct { + mock *MockLogger +} + +// NewMockLogger creates a new mock instance. +func NewMockLogger(ctrl *gomock.Controller) *MockLogger { + mock := &MockLogger{ctrl: ctrl} + mock.recorder = &MockLoggerMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockLogger) EXPECT() *MockLoggerMockRecorder { + return m.recorder +} + +// Debug mocks base method. +func (m *MockLogger) Debug(args ...any) { + m.ctrl.T.Helper() + varargs := []interface{}{} + for _, a := range args { + varargs = append(varargs, a) + } + m.ctrl.Call(m, "Debug", varargs...) +} + +// Debug indicates an expected call of Debug. +func (mr *MockLoggerMockRecorder) Debug(args ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Debug", reflect.TypeOf((*MockLogger)(nil).Debug), args...) +} + +// Debugf mocks base method. +func (m *MockLogger) Debugf(pattern string, args ...any) { + m.ctrl.T.Helper() + varargs := []interface{}{pattern} + for _, a := range args { + varargs = append(varargs, a) + } + m.ctrl.Call(m, "Debugf", varargs...) +} + +// Debugf indicates an expected call of Debugf. +func (mr *MockLoggerMockRecorder) Debugf(pattern interface{}, args ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{pattern}, args...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Debugf", reflect.TypeOf((*MockLogger)(nil).Debugf), varargs...) +} + +// Errorf mocks base method. +func (m *MockLogger) Errorf(pattern string, args ...any) { + m.ctrl.T.Helper() + varargs := []interface{}{pattern} + for _, a := range args { + varargs = append(varargs, a) + } + m.ctrl.Call(m, "Errorf", varargs...) +} + +// Errorf indicates an expected call of Errorf. +func (mr *MockLoggerMockRecorder) Errorf(pattern interface{}, args ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{pattern}, args...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Errorf", reflect.TypeOf((*MockLogger)(nil).Errorf), varargs...) +} + +// Logf mocks base method. +func (m *MockLogger) Logf(pattern string, args ...any) { + m.ctrl.T.Helper() + varargs := []interface{}{pattern} + for _, a := range args { + varargs = append(varargs, a) + } + m.ctrl.Call(m, "Logf", varargs...) +} + +// Logf indicates an expected call of Logf. +func (mr *MockLoggerMockRecorder) Logf(pattern interface{}, args ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{pattern}, args...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Logf", reflect.TypeOf((*MockLogger)(nil).Logf), varargs...) +} + +// MockgcsClient is a mock of gcsClient interface. +type MockgcsClient struct { + ctrl *gomock.Controller + recorder *MockgcsClientMockRecorder +} + +// MockgcsClientMockRecorder is the mock recorder for MockgcsClient. +type MockgcsClientMockRecorder struct { + mock *MockgcsClient +} + +// NewMockgcsClient creates a new mock instance. +func NewMockgcsClient(ctrl *gomock.Controller) *MockgcsClient { + mock := &MockgcsClient{ctrl: ctrl} + mock.recorder = &MockgcsClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockgcsClient) EXPECT() *MockgcsClientMockRecorder { + return m.recorder +} + +// CopyObject mocks base method. +func (m *MockgcsClient) CopyObject(ctx context.Context, src, dst string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CopyObject", ctx, src, dst) + ret0, _ := ret[0].(error) + return ret0 +} + +// CopyObject indicates an expected call of CopyObject. +func (mr *MockgcsClientMockRecorder) CopyObject(ctx, src, dst interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CopyObject", reflect.TypeOf((*MockgcsClient)(nil).CopyObject), ctx, src, dst) +} + +// DeleteObject mocks base method. +func (m *MockgcsClient) DeleteObject(ctx context.Context, name string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteObject", ctx, name) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteObject indicates an expected call of DeleteObject. +func (mr *MockgcsClientMockRecorder) DeleteObject(ctx, name interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteObject", reflect.TypeOf((*MockgcsClient)(nil).DeleteObject), ctx, name) +} + +// ListDir mocks base method. +func (m *MockgcsClient) ListDir(ctx context.Context, prefix string) ([]*storage.ObjectAttrs, []string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListDir", ctx, prefix) + ret0, _ := ret[0].([]*storage.ObjectAttrs) + ret1, _ := ret[1].([]string) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// ListDir indicates an expected call of ListDir. +func (mr *MockgcsClientMockRecorder) ListDir(ctx, prefix interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListDir", reflect.TypeOf((*MockgcsClient)(nil).ListDir), ctx, prefix) +} + +// ListObjects mocks base method. +func (m *MockgcsClient) ListObjects(ctx context.Context, prefix string) ([]string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListObjects", ctx, prefix) + ret0, _ := ret[0].([]string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListObjects indicates an expected call of ListObjects. +func (mr *MockgcsClientMockRecorder) ListObjects(ctx, prefix interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListObjects", reflect.TypeOf((*MockgcsClient)(nil).ListObjects), ctx, prefix) +} + +// NewReader mocks base method. +func (m *MockgcsClient) NewReader(ctx context.Context, name string) (*storage.Reader, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "NewReader", ctx, name) + ret0, _ := ret[0].(*storage.Reader) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// NewReader indicates an expected call of NewReader. +func (mr *MockgcsClientMockRecorder) NewReader(ctx, name interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewReader", reflect.TypeOf((*MockgcsClient)(nil).NewReader), ctx, name) +} + +// NewWriter mocks base method. +func (m *MockgcsClient) NewWriter(ctx context.Context, name string) io.WriteCloser { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "NewWriter", ctx, name) + ret0, _ := ret[0].(io.WriteCloser) + return ret0 +} + +// NewWriter indicates an expected call of NewWriter. +func (mr *MockgcsClientMockRecorder) NewWriter(ctx, name interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewWriter", reflect.TypeOf((*MockgcsClient)(nil).NewWriter), ctx, name) +} + +// StatObject mocks base method. +func (m *MockgcsClient) StatObject(ctx context.Context, name string) (*storage.ObjectAttrs, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "StatObject", ctx, name) + ret0, _ := ret[0].(*storage.ObjectAttrs) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// StatObject indicates an expected call of StatObject. +func (mr *MockgcsClientMockRecorder) StatObject(ctx, name interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StatObject", reflect.TypeOf((*MockgcsClient)(nil).StatObject), ctx, name) +} + +// MockMetrics is a mock of Metrics interface. +type MockMetrics struct { + ctrl *gomock.Controller + recorder *MockMetricsMockRecorder +} + +// MockMetricsMockRecorder is the mock recorder for MockMetrics. +type MockMetricsMockRecorder struct { + mock *MockMetrics +} + +// NewMockMetrics creates a new mock instance. +func NewMockMetrics(ctrl *gomock.Controller) *MockMetrics { + mock := &MockMetrics{ctrl: ctrl} + mock.recorder = &MockMetricsMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockMetrics) EXPECT() *MockMetricsMockRecorder { + return m.recorder +} + +// NewHistogram mocks base method. +func (m *MockMetrics) NewHistogram(name, desc string, buckets ...float64) { + m.ctrl.T.Helper() + varargs := []interface{}{name, desc} + for _, a := range buckets { + varargs = append(varargs, a) + } + m.ctrl.Call(m, "NewHistogram", varargs...) +} + +// NewHistogram indicates an expected call of NewHistogram. +func (mr *MockMetricsMockRecorder) NewHistogram(name, desc interface{}, buckets ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{name, desc}, buckets...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewHistogram", reflect.TypeOf((*MockMetrics)(nil).NewHistogram), varargs...) +} + +// RecordHistogram mocks base method. +func (m *MockMetrics) RecordHistogram(ctx context.Context, name string, value float64, labels ...string) { + m.ctrl.T.Helper() + varargs := []interface{}{ctx, name, value} + for _, a := range labels { + varargs = append(varargs, a) + } + m.ctrl.Call(m, "RecordHistogram", varargs...) +} + +// RecordHistogram indicates an expected call of RecordHistogram. +func (mr *MockMetricsMockRecorder) RecordHistogram(ctx, name, value interface{}, labels ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{ctx, name, value}, labels...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RecordHistogram", reflect.TypeOf((*MockMetrics)(nil).RecordHistogram), varargs...) +}