2015-09-23 01:47:16 +08:00
// Package googlecloudstorage provides an interface to Google Cloud Storage
2014-07-14 00:54:03 +08:00
package googlecloudstorage
/ *
2014-07-15 06:35:41 +08:00
Notes
2014-07-14 00:54:03 +08:00
2014-07-15 06:35:41 +08:00
Can ' t set Updated but can set Metadata on object creation
2014-07-14 00:54:03 +08:00
2014-07-15 06:35:41 +08:00
Patch needs full_control not just read_write
FIXME Patch / Delete / Get isn ' t working with files with spaces in - giving 404 error
- https : //code.google.com/p/google-api-go-client/issues/detail?id=64
2014-07-14 00:54:03 +08:00
* /
import (
2019-03-02 01:05:31 +08:00
"context"
2014-07-14 00:54:03 +08:00
"encoding/base64"
"encoding/hex"
2021-11-04 18:12:57 +08:00
"errors"
2014-07-14 00:54:03 +08:00
"fmt"
"io"
"net/http"
2022-08-20 22:38:02 +08:00
"os"
2014-07-14 17:45:28 +08:00
"path"
2021-05-21 17:11:43 +08:00
"strconv"
2014-07-14 00:54:03 +08:00
"strings"
2022-03-31 22:41:08 +08:00
"sync"
2014-07-14 00:54:03 +08:00
"time"
2019-07-29 01:47:38 +08:00
"github.com/rclone/rclone/fs"
"github.com/rclone/rclone/fs/config"
"github.com/rclone/rclone/fs/config/configmap"
"github.com/rclone/rclone/fs/config/configstruct"
"github.com/rclone/rclone/fs/config/obscure"
"github.com/rclone/rclone/fs/fserrors"
"github.com/rclone/rclone/fs/fshttp"
"github.com/rclone/rclone/fs/hash"
"github.com/rclone/rclone/fs/walk"
2019-08-15 23:26:16 +08:00
"github.com/rclone/rclone/lib/bucket"
2020-01-15 01:33:35 +08:00
"github.com/rclone/rclone/lib/encoder"
2020-06-02 18:54:52 +08:00
"github.com/rclone/rclone/lib/env"
2019-07-29 01:47:38 +08:00
"github.com/rclone/rclone/lib/oauthutil"
"github.com/rclone/rclone/lib/pacer"
2015-08-18 15:55:09 +08:00
"golang.org/x/oauth2"
"golang.org/x/oauth2/google"
2014-12-13 04:02:08 +08:00
"google.golang.org/api/googleapi"
2022-06-25 03:45:38 +08:00
option "google.golang.org/api/option"
2019-03-02 01:05:31 +08:00
// NOTE: This API is deprecated
2017-09-17 04:46:02 +08:00
storage "google.golang.org/api/storage/v1"
2014-07-14 00:54:03 +08:00
)
const (
2016-02-29 03:57:19 +08:00
rcloneClientID = "202264815644.apps.googleusercontent.com"
2016-08-14 19:04:43 +08:00
rcloneEncryptedClientSecret = "Uj7C9jGfb9gmeaV70Lh058cNkWvepr-Es9sBm0zdgil7JaOWF1VySw"
2021-05-21 23:01:32 +08:00
timeFormat = time . RFC3339Nano
2021-05-21 17:11:43 +08:00
metaMtime = "mtime" // key to store mtime in metadata
metaMtimeGsutil = "goog-reserved-file-mtime" // key used by GSUtil to store mtime in metadata
listChunks = 1000 // chunk size to read directory listings
2018-05-09 21:27:21 +08:00
minSleep = 10 * time . Millisecond
2014-07-14 00:54:03 +08:00
)
2024-09-24 15:19:36 +08:00
// Description of how to auth for this app
var storageConfig = & oauth2 . Config {
Scopes : [ ] string { storage . DevstorageReadWriteScope } ,
Endpoint : google . Endpoint ,
ClientID : rcloneClientID ,
ClientSecret : obscure . MustReveal ( rcloneEncryptedClientSecret ) ,
RedirectURL : oauthutil . RedirectURL ,
}
2014-07-14 00:54:03 +08:00
// Register with Fs
func init ( ) {
2016-02-18 19:35:25 +08:00
fs . Register ( & fs . RegInfo {
2016-02-16 02:11:53 +08:00
Name : "google cloud storage" ,
2018-05-15 01:06:57 +08:00
Prefix : "gcs" ,
2016-02-16 02:11:53 +08:00
Description : "Google Cloud Storage (this is not Google Drive)" ,
NewFs : NewFs ,
2021-04-29 16:28:18 +08:00
Config : func ( ctx context . Context , name string , m configmap . Mapper , config fs . ConfigIn ) ( * fs . ConfigOut , error ) {
2018-05-15 01:06:57 +08:00
saFile , _ := m . Get ( "service_account_file" )
saCreds , _ := m . Get ( "service_account_credentials" )
2020-06-30 23:01:02 +08:00
anonymous , _ := m . Get ( "anonymous" )
2023-03-07 02:18:33 +08:00
envAuth , _ := m . Get ( "env_auth" )
if saFile != "" || saCreds != "" || anonymous == "true" || envAuth == "true" {
2021-04-29 16:28:18 +08:00
return nil , nil
2016-04-23 02:58:52 +08:00
}
2021-04-29 16:28:18 +08:00
return oauthutil . ConfigOut ( "" , & oauthutil . Options {
OAuth2Config : storageConfig ,
} )
2014-07-14 00:54:03 +08:00
} ,
2020-08-02 07:32:21 +08:00
Options : append ( oauthutil . SharedOptions , [ ] fs . Option { {
2023-07-07 00:55:53 +08:00
Name : "project_number" ,
Help : "Project number.\n\nOptional - needed only for list/create/delete buckets - see your developer console." ,
Sensitive : true ,
2023-03-12 16:59:21 +08:00
} , {
2023-07-07 00:55:53 +08:00
Name : "user_project" ,
Help : "User project.\n\nOptional - needed only for requester pays." ,
Sensitive : true ,
2016-04-20 22:40:40 +08:00
} , {
Name : "service_account_file" ,
2021-08-16 17:30:01 +08:00
Help : "Service Account Credentials JSON file path.\n\nLeave blank normally.\nNeeded only if you want use SA instead of interactive login." + env . ShellExpandHelp ,
2018-05-15 01:06:57 +08:00
} , {
2023-07-07 00:55:53 +08:00
Name : "service_account_credentials" ,
Help : "Service Account Credentials JSON blob.\n\nLeave blank normally.\nNeeded only if you want use SA instead of interactive login." ,
Hide : fs . OptionHideBoth ,
Sensitive : true ,
2024-09-24 15:19:36 +08:00
} , {
Name : "access_token" ,
Help : "Short-lived access token.\n\nLeave blank normally.\nNeeded only if you want use short-lived access token instead of interactive login." ,
Hide : fs . OptionHideConfigurator ,
Sensitive : true ,
Advanced : true ,
2020-06-30 23:01:02 +08:00
} , {
Name : "anonymous" ,
2021-08-16 17:30:01 +08:00
Help : "Access public buckets and objects without credentials.\n\nSet to 'true' if you just want to download files and don't configure credentials." ,
2020-06-30 23:01:02 +08:00
Default : false ,
2014-07-15 06:35:41 +08:00
} , {
Name : "object_acl" ,
Help : "Access Control List for new objects." ,
Examples : [ ] fs . OptionExample { {
Value : "authenticatedRead" ,
2021-08-16 17:30:01 +08:00
Help : "Object owner gets OWNER access.\nAll Authenticated Users get READER access." ,
2014-07-15 06:35:41 +08:00
} , {
Value : "bucketOwnerFullControl" ,
2021-08-16 17:30:01 +08:00
Help : "Object owner gets OWNER access.\nProject team owners get OWNER access." ,
2014-07-15 06:35:41 +08:00
} , {
Value : "bucketOwnerRead" ,
2021-08-16 17:30:01 +08:00
Help : "Object owner gets OWNER access.\nProject team owners get READER access." ,
2014-07-15 06:35:41 +08:00
} , {
Value : "private" ,
2021-08-16 17:30:01 +08:00
Help : "Object owner gets OWNER access.\nDefault if left blank." ,
2014-07-15 06:35:41 +08:00
} , {
Value : "projectPrivate" ,
2021-08-16 17:30:01 +08:00
Help : "Object owner gets OWNER access.\nProject team members get access according to their roles." ,
2014-07-15 06:35:41 +08:00
} , {
Value : "publicRead" ,
2021-08-16 17:30:01 +08:00
Help : "Object owner gets OWNER access.\nAll Users get READER access." ,
2014-07-15 06:35:41 +08:00
} } ,
} , {
Name : "bucket_acl" ,
Help : "Access Control List for new buckets." ,
Examples : [ ] fs . OptionExample { {
Value : "authenticatedRead" ,
2021-08-16 17:30:01 +08:00
Help : "Project team owners get OWNER access.\nAll Authenticated Users get READER access." ,
2014-07-15 06:35:41 +08:00
} , {
Value : "private" ,
2021-08-16 17:30:01 +08:00
Help : "Project team owners get OWNER access.\nDefault if left blank." ,
2014-07-15 06:35:41 +08:00
} , {
Value : "projectPrivate" ,
Help : "Project team members get access according to their roles." ,
} , {
Value : "publicRead" ,
2021-08-16 17:30:01 +08:00
Help : "Project team owners get OWNER access.\nAll Users get READER access." ,
2014-07-15 06:35:41 +08:00
} , {
Value : "publicReadWrite" ,
2021-08-16 17:30:01 +08:00
Help : "Project team owners get OWNER access.\nAll Users get WRITER access." ,
2014-07-15 06:35:41 +08:00
} } ,
2019-03-04 22:52:54 +08:00
} , {
Name : "bucket_policy_only" ,
Help : ` Access checks should use bucket - level IAM policies .
If you want to upload objects to a bucket with Bucket Policy Only set
then you will need to set this .
When it is set , rclone :
- ignores ACLs set on buckets
- ignores ACLs set on objects
- creates buckets with Bucket Policy Only set
Docs : https : //cloud.google.com/storage/docs/bucket-policy-only
` ,
Default : false ,
2017-07-18 22:15:29 +08:00
} , {
Name : "location" ,
Help : "Location for the newly created buckets." ,
Examples : [ ] fs . OptionExample { {
Value : "" ,
2021-08-16 17:30:01 +08:00
Help : "Empty for default location (US)" ,
2017-07-18 22:15:29 +08:00
} , {
Value : "asia" ,
2021-08-16 17:30:01 +08:00
Help : "Multi-regional location for Asia" ,
2017-07-18 22:15:29 +08:00
} , {
Value : "eu" ,
2021-08-16 17:30:01 +08:00
Help : "Multi-regional location for Europe" ,
2017-07-18 22:15:29 +08:00
} , {
Value : "us" ,
2021-08-16 17:30:01 +08:00
Help : "Multi-regional location for United States" ,
2017-07-18 22:15:29 +08:00
} , {
Value : "asia-east1" ,
2021-08-16 17:30:01 +08:00
Help : "Taiwan" ,
2019-02-03 06:08:30 +08:00
} , {
Value : "asia-east2" ,
2021-08-16 17:30:01 +08:00
Help : "Hong Kong" ,
2017-07-18 22:15:29 +08:00
} , {
Value : "asia-northeast1" ,
2021-08-16 17:30:01 +08:00
Help : "Tokyo" ,
2022-01-28 00:52:22 +08:00
} , {
Value : "asia-northeast2" ,
Help : "Osaka" ,
} , {
Value : "asia-northeast3" ,
Help : "Seoul" ,
2019-02-03 06:08:30 +08:00
} , {
Value : "asia-south1" ,
2021-08-16 17:30:01 +08:00
Help : "Mumbai" ,
2022-01-28 00:52:22 +08:00
} , {
Value : "asia-south2" ,
Help : "Delhi" ,
2017-07-18 22:15:29 +08:00
} , {
Value : "asia-southeast1" ,
2021-08-16 17:30:01 +08:00
Help : "Singapore" ,
2022-01-28 00:52:22 +08:00
} , {
Value : "asia-southeast2" ,
Help : "Jakarta" ,
2017-07-18 22:15:29 +08:00
} , {
Value : "australia-southeast1" ,
2021-08-16 17:30:01 +08:00
Help : "Sydney" ,
2022-01-28 00:52:22 +08:00
} , {
Value : "australia-southeast2" ,
Help : "Melbourne" ,
2019-02-03 06:08:30 +08:00
} , {
Value : "europe-north1" ,
2021-08-16 17:30:01 +08:00
Help : "Finland" ,
2017-07-18 22:15:29 +08:00
} , {
Value : "europe-west1" ,
2021-08-16 17:30:01 +08:00
Help : "Belgium" ,
2017-07-18 22:15:29 +08:00
} , {
Value : "europe-west2" ,
2021-08-16 17:30:01 +08:00
Help : "London" ,
2019-02-03 06:08:30 +08:00
} , {
Value : "europe-west3" ,
2021-08-16 17:30:01 +08:00
Help : "Frankfurt" ,
2019-02-03 06:08:30 +08:00
} , {
Value : "europe-west4" ,
2021-08-16 17:30:01 +08:00
Help : "Netherlands" ,
2022-01-28 00:52:22 +08:00
} , {
Value : "europe-west6" ,
Help : "Zürich" ,
} , {
Value : "europe-central2" ,
Help : "Warsaw" ,
2017-07-18 22:15:29 +08:00
} , {
Value : "us-central1" ,
2021-08-16 17:30:01 +08:00
Help : "Iowa" ,
2017-07-18 22:15:29 +08:00
} , {
Value : "us-east1" ,
2021-08-16 17:30:01 +08:00
Help : "South Carolina" ,
2017-07-18 22:15:29 +08:00
} , {
Value : "us-east4" ,
2021-08-16 17:30:01 +08:00
Help : "Northern Virginia" ,
2017-07-18 22:15:29 +08:00
} , {
Value : "us-west1" ,
2021-08-16 17:30:01 +08:00
Help : "Oregon" ,
2019-02-03 06:08:30 +08:00
} , {
Value : "us-west2" ,
2021-08-16 17:30:01 +08:00
Help : "California" ,
2022-01-28 00:52:22 +08:00
} , {
Value : "us-west3" ,
Help : "Salt Lake City" ,
} , {
Value : "us-west4" ,
Help : "Las Vegas" ,
} , {
Value : "northamerica-northeast1" ,
Help : "Montréal" ,
} , {
Value : "northamerica-northeast2" ,
Help : "Toronto" ,
} , {
Value : "southamerica-east1" ,
Help : "São Paulo" ,
} , {
Value : "southamerica-west1" ,
Help : "Santiago" ,
} , {
Value : "asia1" ,
Help : "Dual region: asia-northeast1 and asia-northeast2." ,
} , {
Value : "eur4" ,
Help : "Dual region: europe-north1 and europe-west4." ,
} , {
Value : "nam4" ,
Help : "Dual region: us-central1 and us-east1." ,
2017-07-18 22:15:29 +08:00
} } ,
} , {
Name : "storage_class" ,
Help : "The storage class to use when storing objects in Google Cloud Storage." ,
Examples : [ ] fs . OptionExample { {
Value : "" ,
Help : "Default" ,
} , {
Value : "MULTI_REGIONAL" ,
Help : "Multi-regional storage class" ,
} , {
Value : "REGIONAL" ,
Help : "Regional storage class" ,
} , {
Value : "NEARLINE" ,
Help : "Nearline storage class" ,
} , {
Value : "COLDLINE" ,
Help : "Coldline storage class" ,
2020-04-27 08:16:57 +08:00
} , {
Value : "ARCHIVE" ,
Help : "Archive storage class" ,
2017-07-18 22:15:29 +08:00
} , {
Value : "DURABLE_REDUCED_AVAILABILITY" ,
Help : "Durable reduced availability storage class" ,
} } ,
2022-09-26 15:43:40 +08:00
} , {
Name : "directory_markers" ,
Default : false ,
Advanced : true ,
2023-04-27 00:53:48 +08:00
Help : ` Upload an empty object with a trailing slash when a new directory is created
2022-09-26 15:43:40 +08:00
2023-04-27 00:53:48 +08:00
Empty folders are unsupported for bucket based remotes , this option creates an empty
object ending with "/" , to persist the folder .
2022-09-26 15:43:40 +08:00
` ,
2022-04-13 03:16:05 +08:00
} , {
Name : "no_check_bucket" ,
Help : ` If set , don ' t attempt to check the bucket exists or create it .
This can be useful when trying to minimise the number of transactions
rclone does if you know the bucket exists already .
` ,
Default : false ,
Advanced : true ,
2022-03-31 22:41:08 +08:00
} , {
2022-07-10 00:31:12 +08:00
Name : "decompress" ,
Help : ` If set this will decompress gzip encoded objects .
2022-03-31 22:41:08 +08:00
It is possible to upload objects to GCS with "Content-Encoding: gzip"
2022-08-14 10:56:32 +08:00
set . Normally rclone will download these files as compressed objects .
2022-03-31 22:41:08 +08:00
2022-07-10 00:31:12 +08:00
If this flag is set then rclone will decompress these files with
2022-03-31 22:41:08 +08:00
"Content-Encoding: gzip" as they are received . This means that rclone
2022-07-10 00:31:12 +08:00
can ' t check the size and hash but the file contents will be decompressed .
2022-03-31 22:41:08 +08:00
` ,
Advanced : true ,
Default : false ,
2022-08-09 18:15:04 +08:00
} , {
Name : "endpoint" ,
Help : "Endpoint for the service.\n\nLeave blank normally." ,
Advanced : true ,
2020-01-15 01:33:35 +08:00
} , {
Name : config . ConfigEncoding ,
Help : config . ConfigEncodingHelp ,
Advanced : true ,
2020-01-15 05:51:49 +08:00
Default : ( encoder . Base |
encoder . EncodeCrLf |
encoder . EncodeInvalidUtf8 ) ,
2023-03-07 02:18:33 +08:00
} , {
Name : "env_auth" ,
Help : "Get GCP IAM credentials from runtime (environment variables or instance meta data if no env vars).\n\nOnly applies if service_account_file and service_account_credentials is blank." ,
Default : false ,
Examples : [ ] fs . OptionExample { {
Value : "false" ,
2023-03-07 19:39:02 +08:00
Help : "Enter credentials in the next step." ,
2023-03-07 02:18:33 +08:00
} , {
Value : "true" ,
Help : "Get GCP IAM credentials from the environment (env vars or IAM)." ,
} } ,
2020-08-02 07:32:21 +08:00
} } ... ) ,
2014-07-14 00:54:03 +08:00
} )
}
2018-05-15 01:06:57 +08:00
// Options defines the configuration for this backend
type Options struct {
2020-01-15 01:33:35 +08:00
ProjectNumber string ` config:"project_number" `
2023-03-12 16:59:21 +08:00
UserProject string ` config:"user_project" `
2020-01-15 01:33:35 +08:00
ServiceAccountFile string ` config:"service_account_file" `
ServiceAccountCredentials string ` config:"service_account_credentials" `
2020-06-30 23:01:02 +08:00
Anonymous bool ` config:"anonymous" `
2020-01-15 01:33:35 +08:00
ObjectACL string ` config:"object_acl" `
BucketACL string ` config:"bucket_acl" `
BucketPolicyOnly bool ` config:"bucket_policy_only" `
Location string ` config:"location" `
StorageClass string ` config:"storage_class" `
2022-04-13 03:16:05 +08:00
NoCheckBucket bool ` config:"no_check_bucket" `
2022-07-10 00:31:12 +08:00
Decompress bool ` config:"decompress" `
2022-08-09 18:15:04 +08:00
Endpoint string ` config:"endpoint" `
2020-01-15 01:33:35 +08:00
Enc encoder . MultiEncoder ` config:"encoding" `
2023-03-07 02:18:33 +08:00
EnvAuth bool ` config:"env_auth" `
2022-09-26 15:43:40 +08:00
DirectoryMarkers bool ` config:"directory_markers" `
2024-09-24 15:19:36 +08:00
AccessToken string ` config:"access_token" `
2018-05-15 01:06:57 +08:00
}
2015-11-07 19:14:46 +08:00
// Fs represents a remote storage server
type Fs struct {
2022-03-31 22:41:08 +08:00
name string // name of this remote
root string // the path we are working on if any
opt Options // parsed options
features * fs . Features // optional features
svc * storage . Service // the connection to the storage server
client * http . Client // authorized client
rootBucket string // bucket part of root (if any)
rootDirectory string // directory part of root (if any)
cache * bucket . Cache // cache of bucket status
pacer * fs . Pacer // To pace the API calls
warnCompressed sync . Once // warn once about compressed files
2014-07-14 00:54:03 +08:00
}
2015-11-07 19:14:46 +08:00
// Object describes a storage object
2014-07-14 00:54:03 +08:00
//
// Will definitely have info but maybe not meta
2015-11-07 19:14:46 +08:00
type Object struct {
2016-09-22 05:13:24 +08:00
fs * Fs // what this object is part of
remote string // The remote path
url string // download path
md5sum string // The MD5Sum of the object
bytes int64 // Bytes in the object
modTime time . Time // Modified time of the object
mimeType string
2022-03-31 22:41:08 +08:00
gzipped bool // set if object has Content-Encoding: gzip
2014-07-14 00:54:03 +08:00
}
// ------------------------------------------------------------
2015-09-23 01:47:16 +08:00
// Name of the remote (as passed into NewFs)
2015-11-07 19:14:46 +08:00
func ( f * Fs ) Name ( ) string {
2015-08-22 23:53:11 +08:00
return f . name
}
2015-09-23 01:47:16 +08:00
// Root of the remote (as passed into NewFs)
2015-11-07 19:14:46 +08:00
func ( f * Fs ) Root ( ) string {
2019-08-15 23:26:16 +08:00
return f . root
2015-09-02 03:45:27 +08:00
}
2015-11-07 19:14:46 +08:00
// String converts this Fs to a string
func ( f * Fs ) String ( ) string {
2019-08-15 23:26:16 +08:00
if f . rootBucket == "" {
2022-06-09 04:25:17 +08:00
return "GCS root"
2019-08-15 23:26:16 +08:00
}
if f . rootDirectory == "" {
return fmt . Sprintf ( "GCS bucket %s" , f . rootBucket )
2014-07-14 00:54:03 +08:00
}
2019-08-15 23:26:16 +08:00
return fmt . Sprintf ( "GCS bucket %s path %s" , f . rootBucket , f . rootDirectory )
2014-07-14 00:54:03 +08:00
}
2017-01-14 01:21:47 +08:00
// Features returns the optional features of this Fs
func ( f * Fs ) Features ( ) * fs . Features {
return f . features
}
2019-02-08 01:41:17 +08:00
// shouldRetry determines whether a given err rates being retried
2021-03-11 22:44:01 +08:00
func shouldRetry ( ctx context . Context , err error ) ( again bool , errOut error ) {
if fserrors . ContextError ( ctx , & err ) {
return false , err
}
2018-05-09 21:27:21 +08:00
again = false
if err != nil {
if fserrors . ShouldRetry ( err ) {
again = true
} else {
switch gerr := err . ( type ) {
case * googleapi . Error :
if gerr . Code >= 500 && gerr . Code < 600 {
// All 5xx errors should be retried
again = true
} else if len ( gerr . Errors ) > 0 {
reason := gerr . Errors [ 0 ] . Reason
if reason == "rateLimitExceeded" || reason == "userRateLimitExceeded" {
again = true
}
}
}
}
}
return again , err
}
2019-08-15 23:26:16 +08:00
// parsePath parses a remote 'url'
func parsePath ( path string ) ( root string ) {
root = strings . Trim ( path , "/" )
2014-07-14 00:54:03 +08:00
return
}
2019-08-15 23:26:16 +08:00
// split returns bucket and bucketPath from the rootRelativePath
// relative to f.root
func ( f * Fs ) split ( rootRelativePath string ) ( bucketName , bucketPath string ) {
2022-09-26 15:43:40 +08:00
bucketName , bucketPath = bucket . Split ( bucket . Join ( f . root , rootRelativePath ) )
2020-01-15 01:33:35 +08:00
return f . opt . Enc . FromStandardName ( bucketName ) , f . opt . Enc . FromStandardPath ( bucketPath )
2019-08-15 23:26:16 +08:00
}
// split returns bucket and bucketPath from the object
func ( o * Object ) split ( ) ( bucket , bucketPath string ) {
return o . fs . split ( o . remote )
}
2020-11-06 02:02:26 +08:00
func getServiceAccountClient ( ctx context . Context , credentialsData [ ] byte ) ( * http . Client , error ) {
2018-04-27 23:07:37 +08:00
conf , err := google . JWTConfigFromJSON ( credentialsData , storageConfig . Scopes ... )
2016-04-20 22:40:40 +08:00
if err != nil {
2021-11-04 18:12:57 +08:00
return nil , fmt . Errorf ( "error processing credentials: %w" , err )
2016-04-20 22:40:40 +08:00
}
2020-11-13 23:24:43 +08:00
ctxWithSpecialClient := oauthutil . Context ( ctx , fshttp . NewClient ( ctx ) )
2016-04-20 22:40:40 +08:00
return oauth2 . NewClient ( ctxWithSpecialClient , conf . TokenSource ( ctxWithSpecialClient ) ) , nil
}
2019-08-15 23:26:16 +08:00
// setRoot changes the root of the Fs
func ( f * Fs ) setRoot ( root string ) {
f . root = parsePath ( root )
f . rootBucket , f . rootDirectory = bucket . Split ( f . root )
}
2019-02-08 01:41:17 +08:00
// NewFs constructs an Fs from the path, bucket:path
2020-11-05 23:18:51 +08:00
func NewFs ( ctx context . Context , name , root string , m configmap . Mapper ) ( fs . Fs , error ) {
2016-04-20 22:40:40 +08:00
var oAuthClient * http . Client
2018-05-15 01:06:57 +08:00
// Parse config into Options struct
opt := new ( Options )
err := configstruct . Set ( m , opt )
if err != nil {
return nil , err
}
if opt . ObjectACL == "" {
opt . ObjectACL = "private"
}
if opt . BucketACL == "" {
opt . BucketACL = "private"
}
2016-04-20 22:40:40 +08:00
2018-04-27 23:07:37 +08:00
// try loading service account credentials from env variable, then from a file
2018-09-04 18:28:45 +08:00
if opt . ServiceAccountCredentials == "" && opt . ServiceAccountFile != "" {
2022-08-20 22:38:02 +08:00
loadedCreds , err := os . ReadFile ( env . ShellExpand ( opt . ServiceAccountFile ) )
2018-04-27 23:07:37 +08:00
if err != nil {
2021-11-04 18:12:57 +08:00
return nil , fmt . Errorf ( "error opening service account credentials file: %w" , err )
2018-04-27 23:07:37 +08:00
}
2018-05-15 01:06:57 +08:00
opt . ServiceAccountCredentials = string ( loadedCreds )
2018-04-27 23:07:37 +08:00
}
2020-06-30 23:01:02 +08:00
if opt . Anonymous {
2021-01-19 23:10:35 +08:00
oAuthClient = fshttp . NewClient ( ctx )
2020-06-30 23:01:02 +08:00
} else if opt . ServiceAccountCredentials != "" {
2020-11-06 02:02:26 +08:00
oAuthClient , err = getServiceAccountClient ( ctx , [ ] byte ( opt . ServiceAccountCredentials ) )
2016-04-20 22:40:40 +08:00
if err != nil {
2021-11-04 18:12:57 +08:00
return nil , fmt . Errorf ( "failed configuring Google Cloud Storage Service Account: %w" , err )
2016-04-20 22:40:40 +08:00
}
2023-03-07 02:18:33 +08:00
} else if opt . EnvAuth {
oAuthClient , err = google . DefaultClient ( ctx , storage . DevstorageFullControlScope )
if err != nil {
return nil , fmt . Errorf ( "failed to configure Google Cloud Storage: %w" , err )
}
2024-09-24 15:19:36 +08:00
} else if opt . AccessToken != "" {
ts := oauth2 . Token { AccessToken : opt . AccessToken }
oAuthClient = oauth2 . NewClient ( ctx , oauth2 . StaticTokenSource ( & ts ) )
2016-04-20 22:40:40 +08:00
} else {
2020-11-06 02:02:26 +08:00
oAuthClient , _ , err = oauthutil . NewClient ( ctx , name , m , storageConfig )
2016-04-20 22:40:40 +08:00
if err != nil {
2019-03-02 01:05:31 +08:00
ctx := context . Background ( )
oAuthClient , err = google . DefaultClient ( ctx , storage . DevstorageFullControlScope )
if err != nil {
2021-11-04 18:12:57 +08:00
return nil , fmt . Errorf ( "failed to configure Google Cloud Storage: %w" , err )
2019-03-02 01:05:31 +08:00
}
2016-04-20 22:40:40 +08:00
}
2014-07-14 00:54:03 +08:00
}
2015-11-07 19:14:46 +08:00
f := & Fs {
2019-08-15 23:26:16 +08:00
name : name ,
root : root ,
opt : * opt ,
2022-03-25 16:58:39 +08:00
pacer : fs . NewPacer ( ctx , pacer . NewS3 ( pacer . MinSleep ( minSleep ) ) ) ,
2019-08-15 23:26:16 +08:00
cache : bucket . NewCache ( ) ,
2014-07-15 06:35:41 +08:00
}
2019-08-15 23:26:16 +08:00
f . setRoot ( root )
2017-08-09 22:27:43 +08:00
f . features = ( & fs . Features {
2019-08-15 23:26:16 +08:00
ReadMimeType : true ,
WriteMimeType : true ,
BucketBased : true ,
BucketBasedRootOK : true ,
2020-11-06 00:00:40 +08:00
} ) . Fill ( ctx , f )
2023-04-27 00:53:48 +08:00
if opt . DirectoryMarkers {
f . features . CanHaveEmptyDirectories = true
}
2014-07-14 00:54:03 +08:00
// Create a new authorized Drive client.
2015-08-18 15:55:09 +08:00
f . client = oAuthClient
2022-08-09 18:15:04 +08:00
gcsOpts := [ ] option . ClientOption { option . WithHTTPClient ( f . client ) }
if opt . Endpoint != "" {
gcsOpts = append ( gcsOpts , option . WithEndpoint ( opt . Endpoint ) )
}
f . svc , err = storage . NewService ( context . Background ( ) , gcsOpts ... )
2014-07-14 00:54:03 +08:00
if err != nil {
2021-11-04 18:12:57 +08:00
return nil , fmt . Errorf ( "couldn't create Google Cloud Storage client: %w" , err )
2014-07-14 00:54:03 +08:00
}
2019-08-15 23:26:16 +08:00
if f . rootBucket != "" && f . rootDirectory != "" {
2014-07-14 00:54:03 +08:00
// Check to see if the object exists
2020-01-15 01:33:35 +08:00
encodedDirectory := f . opt . Enc . FromStandardPath ( f . rootDirectory )
2018-05-09 21:27:21 +08:00
err = f . pacer . Call ( func ( ) ( bool , error ) {
2023-03-12 16:59:21 +08:00
get := f . svc . Objects . Get ( f . rootBucket , encodedDirectory ) . Context ( ctx )
if f . opt . UserProject != "" {
get = get . UserProject ( f . opt . UserProject )
}
_ , err = get . Do ( )
2021-03-11 22:44:01 +08:00
return shouldRetry ( ctx , err )
2018-05-09 21:27:21 +08:00
} )
2014-07-14 17:45:28 +08:00
if err == nil {
2019-08-15 23:26:16 +08:00
newRoot := path . Dir ( f . root )
if newRoot == "." {
newRoot = ""
2014-07-14 17:45:28 +08:00
}
2019-08-15 23:26:16 +08:00
f . setRoot ( newRoot )
2016-06-22 01:01:53 +08:00
// return an error with an fs which points to the parent
return f , fs . ErrorIsFile
2014-07-14 17:45:28 +08:00
}
2014-07-14 00:54:03 +08:00
}
return f , nil
}
2016-06-26 04:58:34 +08:00
// Return an Object from a path
2014-07-14 00:54:03 +08:00
//
2016-06-26 04:23:20 +08:00
// If it can't be found it returns the error fs.ErrorObjectNotFound.
2019-09-06 20:50:36 +08:00
func ( f * Fs ) newObjectWithInfo ( ctx context . Context , remote string , info * storage . Object ) ( fs . Object , error ) {
2015-11-07 19:14:46 +08:00
o := & Object {
fs : f ,
remote : remote ,
2014-07-14 00:54:03 +08:00
}
if info != nil {
o . setMetaData ( info )
} else {
2019-09-06 20:50:36 +08:00
err := o . readMetaData ( ctx ) // reads info and meta, returning an error
2014-07-14 00:54:03 +08:00
if err != nil {
2016-06-26 04:23:20 +08:00
return nil , err
2014-07-14 00:54:03 +08:00
}
}
2016-06-26 04:23:20 +08:00
return o , nil
2014-07-14 00:54:03 +08:00
}
2016-06-26 04:23:20 +08:00
// NewObject finds the Object at remote. If it can't be found
// it returns the error fs.ErrorObjectNotFound.
2019-06-17 16:34:30 +08:00
func ( f * Fs ) NewObject ( ctx context . Context , remote string ) ( fs . Object , error ) {
2019-09-06 20:50:36 +08:00
return f . newObjectWithInfo ( ctx , remote , nil )
2014-07-14 00:54:03 +08:00
}
2016-04-22 03:06:21 +08:00
// listFn is called from list to handle an object.
type listFn func ( remote string , object * storage . Object , isDirectory bool ) error
2014-07-14 00:54:03 +08:00
// list the objects into the function supplied
//
2016-04-24 04:46:52 +08:00
// dir is the starting directory, "" for root
//
2022-08-05 23:35:41 +08:00
// Set recurse to read sub directories.
2019-08-15 23:26:16 +08:00
//
// The remote has prefix removed from it and if addBucket is set
// then it adds the bucket to the start.
func ( f * Fs ) list ( ctx context . Context , bucket , directory , prefix string , addBucket bool , recurse bool , fn listFn ) ( err error ) {
if prefix != "" {
prefix += "/"
}
if directory != "" {
directory += "/"
2016-04-24 04:46:52 +08:00
}
2019-08-15 23:26:16 +08:00
list := f . svc . Objects . List ( bucket ) . Prefix ( directory ) . MaxResults ( listChunks )
2023-03-12 16:59:21 +08:00
if f . opt . UserProject != "" {
list = list . UserProject ( f . opt . UserProject )
}
2017-06-12 05:43:31 +08:00
if ! recurse {
2014-07-14 00:54:03 +08:00
list = list . Delimiter ( "/" )
}
2023-04-27 00:53:48 +08:00
foundItems := 0
2014-07-14 00:54:03 +08:00
for {
2018-05-09 21:27:21 +08:00
var objects * storage . Objects
err = f . pacer . Call ( func ( ) ( bool , error ) {
2019-09-06 20:50:36 +08:00
objects , err = list . Context ( ctx ) . Do ( )
2021-03-11 22:44:01 +08:00
return shouldRetry ( ctx , err )
2018-05-09 21:27:21 +08:00
} )
2014-07-14 00:54:03 +08:00
if err != nil {
2017-06-12 05:43:31 +08:00
if gErr , ok := err . ( * googleapi . Error ) ; ok {
if gErr . Code == http . StatusNotFound {
err = fs . ErrorDirNotFound
}
}
2016-04-22 03:06:21 +08:00
return err
2014-07-14 00:54:03 +08:00
}
2017-06-12 05:43:31 +08:00
if ! recurse {
2023-04-27 00:53:48 +08:00
foundItems += len ( objects . Prefixes )
2014-07-29 01:04:52 +08:00
var object storage . Object
2019-08-15 23:26:16 +08:00
for _ , remote := range objects . Prefixes {
if ! strings . HasSuffix ( remote , "/" ) {
2014-07-29 01:04:52 +08:00
continue
}
2020-01-15 01:33:35 +08:00
remote = f . opt . Enc . ToStandardPath ( remote )
2019-08-15 23:26:16 +08:00
if ! strings . HasPrefix ( remote , prefix ) {
fs . Logf ( f , "Odd name received %q" , remote )
continue
}
remote = remote [ len ( prefix ) : len ( remote ) - 1 ]
if addBucket {
remote = path . Join ( bucket , remote )
}
err = fn ( remote , & object , true )
2016-04-22 03:06:21 +08:00
if err != nil {
return err
}
}
}
2023-04-27 00:53:48 +08:00
foundItems += len ( objects . Items )
2016-04-22 03:06:21 +08:00
for _ , object := range objects . Items {
2020-01-15 01:33:35 +08:00
remote := f . opt . Enc . ToStandardPath ( object . Name )
2019-05-19 23:54:46 +08:00
if ! strings . HasPrefix ( remote , prefix ) {
2017-02-09 19:01:20 +08:00
fs . Logf ( f , "Odd name received %q" , object . Name )
2016-04-22 03:06:21 +08:00
continue
}
2020-03-31 18:44:24 +08:00
isDirectory := remote == "" || strings . HasSuffix ( remote , "/" )
2018-03-20 01:42:27 +08:00
// is this a directory marker?
2021-01-19 23:13:52 +08:00
if isDirectory {
2023-04-27 00:53:48 +08:00
// Don't insert the root directory
2024-04-29 15:21:49 +08:00
if remote == f . opt . Enc . ToStandardPath ( directory ) {
2023-04-27 00:53:48 +08:00
continue
}
// process directory markers as directories
remote = strings . TrimRight ( remote , "/" )
2018-03-20 01:42:27 +08:00
}
2023-06-10 21:18:59 +08:00
remote = remote [ len ( prefix ) : ]
if addBucket {
remote = path . Join ( bucket , remote )
}
2023-04-27 00:53:48 +08:00
err = fn ( remote , object , isDirectory )
2016-04-22 03:06:21 +08:00
if err != nil {
return err
2014-07-14 00:54:03 +08:00
}
}
if objects . NextPageToken == "" {
break
}
list . PageToken ( objects . NextPageToken )
}
2023-04-27 00:53:48 +08:00
if f . opt . DirectoryMarkers && foundItems == 0 && directory != "" {
// Determine whether the directory exists or not by whether it has a marker
_ , err := f . readObjectInfo ( ctx , bucket , directory )
if err != nil {
if err == fs . ErrorObjectNotFound {
return fs . ErrorDirNotFound
}
return err
}
}
2016-04-22 03:06:21 +08:00
return nil
2014-07-14 00:54:03 +08:00
}
2017-06-30 17:54:14 +08:00
// Convert a list item into a DirEntry
2019-09-06 20:50:36 +08:00
func ( f * Fs ) itemToDirEntry ( ctx context . Context , remote string , object * storage . Object , isDirectory bool ) ( fs . DirEntry , error ) {
2017-06-12 05:43:31 +08:00
if isDirectory {
2017-06-30 20:37:29 +08:00
d := fs . NewDir ( remote , time . Time { } ) . SetSize ( int64 ( object . Size ) )
2017-06-12 05:43:31 +08:00
return d , nil
2016-04-22 03:06:21 +08:00
}
2019-09-06 20:50:36 +08:00
o , err := f . newObjectWithInfo ( ctx , remote , object )
2017-06-12 05:43:31 +08:00
if err != nil {
return nil , err
}
return o , nil
}
// listDir lists a single directory
2019-08-15 23:26:16 +08:00
func ( f * Fs ) listDir ( ctx context . Context , bucket , directory , prefix string , addBucket bool ) ( entries fs . DirEntries , err error ) {
2016-04-22 03:06:21 +08:00
// List the objects
2019-08-15 23:26:16 +08:00
err = f . list ( ctx , bucket , directory , prefix , addBucket , false , func ( remote string , object * storage . Object , isDirectory bool ) error {
2019-09-06 20:50:36 +08:00
entry , err := f . itemToDirEntry ( ctx , remote , object , isDirectory )
2017-06-12 05:43:31 +08:00
if err != nil {
return err
}
if entry != nil {
entries = append ( entries , entry )
2016-04-22 03:06:21 +08:00
}
return nil
} )
if err != nil {
2017-06-12 05:43:31 +08:00
return nil , err
2014-07-14 00:54:03 +08:00
}
2018-03-01 20:11:34 +08:00
// bucket must be present if listing succeeded
2019-08-15 23:26:16 +08:00
f . cache . MarkOK ( bucket )
2017-06-12 05:43:31 +08:00
return entries , err
2014-07-14 00:54:03 +08:00
}
2017-06-12 05:43:31 +08:00
// listBuckets lists the buckets
2019-08-23 04:30:55 +08:00
func ( f * Fs ) listBuckets ( ctx context . Context ) ( entries fs . DirEntries , err error ) {
2018-05-15 01:06:57 +08:00
if f . opt . ProjectNumber == "" {
2017-06-12 05:43:31 +08:00
return nil , errors . New ( "can't list buckets without project number" )
2016-04-22 03:06:21 +08:00
}
2018-05-15 01:06:57 +08:00
listBuckets := f . svc . Buckets . List ( f . opt . ProjectNumber ) . MaxResults ( listChunks )
2023-03-12 16:59:21 +08:00
if f . opt . UserProject != "" {
listBuckets = listBuckets . UserProject ( f . opt . UserProject )
}
2016-04-22 03:06:21 +08:00
for {
2018-05-09 21:27:21 +08:00
var buckets * storage . Buckets
err = f . pacer . Call ( func ( ) ( bool , error ) {
2019-09-06 20:50:36 +08:00
buckets , err = listBuckets . Context ( ctx ) . Do ( )
2021-03-11 22:44:01 +08:00
return shouldRetry ( ctx , err )
2018-05-09 21:27:21 +08:00
} )
2016-04-22 03:06:21 +08:00
if err != nil {
2017-06-12 05:43:31 +08:00
return nil , err
2016-04-22 03:06:21 +08:00
}
for _ , bucket := range buckets . Items {
2020-01-15 01:33:35 +08:00
d := fs . NewDir ( f . opt . Enc . ToStandardName ( bucket . Name ) , time . Time { } )
2017-06-12 05:43:31 +08:00
entries = append ( entries , d )
2016-04-22 03:06:21 +08:00
}
if buckets . NextPageToken == "" {
break
}
listBuckets . PageToken ( buckets . NextPageToken )
}
2017-06-12 05:43:31 +08:00
return entries , nil
2016-04-22 03:06:21 +08:00
}
2017-06-12 05:43:31 +08:00
// List the objects and directories in dir into entries. The
// entries can be returned in any order but should be for a
// complete directory.
//
// dir should be "" to list the root, and should not have
// trailing slashes.
//
// This should return ErrDirNotFound if the directory isn't
// found.
2019-06-17 16:34:30 +08:00
func ( f * Fs ) List ( ctx context . Context , dir string ) ( entries fs . DirEntries , err error ) {
2019-08-15 23:26:16 +08:00
bucket , directory := f . split ( dir )
if bucket == "" {
2019-08-23 04:30:55 +08:00
if directory != "" {
return nil , fs . ErrorListBucketRequired
}
return f . listBuckets ( ctx )
2014-07-14 00:54:03 +08:00
}
2019-08-15 23:26:16 +08:00
return f . listDir ( ctx , bucket , directory , f . rootDirectory , f . rootBucket == "" )
2014-07-14 00:54:03 +08:00
}
2017-06-05 23:14:24 +08:00
// ListR lists the objects and directories of the Fs starting
// from dir recursively into out.
2017-06-12 05:43:31 +08:00
//
// dir should be "" to start from the root, and should not
// have trailing slashes.
//
// This should return ErrDirNotFound if the directory isn't
// found.
//
// It should call callback for each tranche of entries read.
// These need not be returned in any particular order. If
// callback returns an error then the listing will stop
// immediately.
//
// Don't implement this unless you have a more efficient way
// of listing recursively that doing a directory traversal.
2019-06-17 16:34:30 +08:00
func ( f * Fs ) ListR ( ctx context . Context , dir string , callback fs . ListRCallback ) ( err error ) {
2019-08-15 23:26:16 +08:00
bucket , directory := f . split ( dir )
2018-01-13 00:30:54 +08:00
list := walk . NewListRHelper ( callback )
2019-08-15 23:26:16 +08:00
listR := func ( bucket , directory , prefix string , addBucket bool ) error {
return f . list ( ctx , bucket , directory , prefix , addBucket , true , func ( remote string , object * storage . Object , isDirectory bool ) error {
2019-09-06 20:50:36 +08:00
entry , err := f . itemToDirEntry ( ctx , remote , object , isDirectory )
2019-08-15 23:26:16 +08:00
if err != nil {
return err
}
return list . Add ( entry )
} )
}
if bucket == "" {
2019-08-23 04:30:55 +08:00
entries , err := f . listBuckets ( ctx )
2019-08-15 23:26:16 +08:00
if err != nil {
return err
}
for _ , entry := range entries {
err = list . Add ( entry )
if err != nil {
return err
}
bucket := entry . Remote ( )
err = listR ( bucket , "" , f . rootDirectory , true )
if err != nil {
return err
}
2019-08-23 04:30:55 +08:00
// bucket must be present if listing succeeded
f . cache . MarkOK ( bucket )
2019-08-15 23:26:16 +08:00
}
} else {
err = listR ( bucket , directory , f . rootDirectory , f . rootBucket == "" )
2017-06-12 05:43:31 +08:00
if err != nil {
return err
}
2019-08-23 04:30:55 +08:00
// bucket must be present if listing succeeded
f . cache . MarkOK ( bucket )
2017-06-12 05:43:31 +08:00
}
return list . Flush ( )
2017-06-05 23:14:24 +08:00
}
2014-07-14 00:54:03 +08:00
// Put the object into the bucket
//
2022-08-05 23:35:41 +08:00
// Copy the reader in to the new object which is returned.
2014-07-14 00:54:03 +08:00
//
// The new object may have been created if an error is returned
2019-06-17 16:34:30 +08:00
func ( f * Fs ) Put ( ctx context . Context , in io . Reader , src fs . ObjectInfo , options ... fs . OpenOption ) ( fs . Object , error ) {
2015-11-07 19:14:46 +08:00
// Temporary Object under construction
o := & Object {
fs : f ,
2016-02-18 19:35:25 +08:00
remote : src . Remote ( ) ,
2015-11-07 19:14:46 +08:00
}
2019-06-17 16:34:30 +08:00
return o , o . Update ( ctx , in , src , options ... )
2014-07-14 00:54:03 +08:00
}
2017-09-17 04:46:02 +08:00
// PutStream uploads to the remote path with the modTime given of indeterminate size
2019-06-17 16:34:30 +08:00
func ( f * Fs ) PutStream ( ctx context . Context , in io . Reader , src fs . ObjectInfo , options ... fs . OpenOption ) ( fs . Object , error ) {
return f . Put ( ctx , in , src , options ... )
2017-09-17 04:46:02 +08:00
}
2023-04-27 00:53:48 +08:00
// Create directory marker file and parents
func ( f * Fs ) createDirectoryMarker ( ctx context . Context , bucket , dir string ) error {
if ! f . opt . DirectoryMarkers || bucket == "" {
return nil
}
// Object to be uploaded
o := & Object {
2023-04-29 00:23:37 +08:00
fs : f ,
modTime : time . Now ( ) ,
2023-04-27 00:53:48 +08:00
}
for {
_ , bucketPath := f . split ( dir )
// Don't create the directory marker if it is the bucket or at the very root
if bucketPath == "" {
break
}
o . remote = dir + "/"
// Check to see if object already exists
_ , err := o . readObjectInfo ( ctx )
if err == nil {
return nil
}
// Upload it if not
fs . Debugf ( o , "Creating directory marker" )
content := io . Reader ( strings . NewReader ( "" ) )
2023-04-29 00:23:37 +08:00
err = o . Update ( ctx , content , o )
2023-04-27 00:53:48 +08:00
if err != nil {
return fmt . Errorf ( "creating directory marker failed: %w" , err )
}
// Now check parent directory exists
dir = path . Dir ( dir )
if dir == "/" || dir == "." {
break
}
}
return nil
}
2014-07-14 00:54:03 +08:00
// Mkdir creates the bucket if it doesn't exist
2019-06-17 16:34:30 +08:00
func ( f * Fs ) Mkdir ( ctx context . Context , dir string ) ( err error ) {
2019-08-15 23:26:16 +08:00
bucket , _ := f . split ( dir )
2023-04-27 00:53:48 +08:00
e := f . checkBucket ( ctx , bucket )
2022-09-26 15:43:40 +08:00
if e != nil {
return e
}
2023-04-27 00:53:48 +08:00
return f . createDirectoryMarker ( ctx , bucket , dir )
}
// mkdirParent creates the parent bucket/directory if it doesn't exist
func ( f * Fs ) mkdirParent ( ctx context . Context , remote string ) error {
remote = strings . TrimRight ( remote , "/" )
dir := path . Dir ( remote )
if dir == "/" || dir == "." {
dir = ""
2022-09-26 15:43:40 +08:00
}
2023-04-27 00:53:48 +08:00
return f . Mkdir ( ctx , dir )
2019-08-23 04:30:55 +08:00
}
// makeBucket creates the bucket if it doesn't exist
func ( f * Fs ) makeBucket ( ctx context . Context , bucket string ) ( err error ) {
2019-08-15 23:26:16 +08:00
return f . cache . Create ( bucket , func ( ) error {
// List something from the bucket to see if it exists. Doing it like this enables the use of a
// service account that only has the "Storage Object Admin" role. See #2193 for details.
err = f . pacer . Call ( func ( ) ( bool , error ) {
2023-03-12 16:59:21 +08:00
list := f . svc . Objects . List ( bucket ) . MaxResults ( 1 ) . Context ( ctx )
if f . opt . UserProject != "" {
list = list . UserProject ( f . opt . UserProject )
}
_ , err = list . Do ( )
2021-03-11 22:44:01 +08:00
return shouldRetry ( ctx , err )
2019-08-15 23:26:16 +08:00
} )
if err == nil {
// Bucket already exists
return nil
} else if gErr , ok := err . ( * googleapi . Error ) ; ok {
if gErr . Code != http . StatusNotFound {
2021-11-04 18:12:57 +08:00
return fmt . Errorf ( "failed to get bucket: %w" , err )
2019-08-15 23:26:16 +08:00
}
} else {
2021-11-04 18:12:57 +08:00
return fmt . Errorf ( "failed to get bucket: %w" , err )
2017-08-10 17:29:21 +08:00
}
2014-07-14 00:54:03 +08:00
2019-08-15 23:26:16 +08:00
if f . opt . ProjectNumber == "" {
return errors . New ( "can't make bucket without project number" )
}
2014-07-14 00:54:03 +08:00
2019-08-15 23:26:16 +08:00
bucket := storage . Bucket {
Name : bucket ,
Location : f . opt . Location ,
StorageClass : f . opt . StorageClass ,
2019-03-04 22:52:54 +08:00
}
2019-08-15 23:26:16 +08:00
if f . opt . BucketPolicyOnly {
bucket . IamConfiguration = & storage . BucketIamConfiguration {
BucketPolicyOnly : & storage . BucketIamConfigurationBucketPolicyOnly {
Enabled : true ,
} ,
}
2019-03-04 22:52:54 +08:00
}
2019-08-15 23:26:16 +08:00
return f . pacer . Call ( func ( ) ( bool , error ) {
insertBucket := f . svc . Buckets . Insert ( f . opt . ProjectNumber , & bucket )
if ! f . opt . BucketPolicyOnly {
insertBucket . PredefinedAcl ( f . opt . BucketACL )
}
2023-03-12 16:59:21 +08:00
insertBucket = insertBucket . Context ( ctx )
if f . opt . UserProject != "" {
insertBucket = insertBucket . UserProject ( f . opt . UserProject )
}
_ , err = insertBucket . Do ( )
2021-03-11 22:44:01 +08:00
return shouldRetry ( ctx , err )
2019-08-15 23:26:16 +08:00
} )
} , nil )
2014-07-14 00:54:03 +08:00
}
2022-04-13 03:16:05 +08:00
// checkBucket creates the bucket if it doesn't exist unless NoCheckBucket is true
func ( f * Fs ) checkBucket ( ctx context . Context , bucket string ) error {
if f . opt . NoCheckBucket {
return nil
}
return f . makeBucket ( ctx , bucket )
}
2015-11-07 23:31:04 +08:00
// Rmdir deletes the bucket if the fs is at the root
2014-07-14 00:54:03 +08:00
//
// Returns an error if it isn't empty: Error 409: The bucket you tried
// to delete was not empty.
2019-06-17 16:34:30 +08:00
func ( f * Fs ) Rmdir ( ctx context . Context , dir string ) ( err error ) {
2019-08-15 23:26:16 +08:00
bucket , directory := f . split ( dir )
2022-09-26 15:43:40 +08:00
// Remove directory marker file
if f . opt . DirectoryMarkers && bucket != "" && dir != "" {
2023-04-27 00:53:48 +08:00
o := & Object {
2022-09-26 15:43:40 +08:00
fs : f ,
2023-04-27 00:53:48 +08:00
remote : dir + "/" ,
}
fs . Debugf ( o , "Removing directory marker" )
err := o . Remove ( ctx )
if err != nil {
return fmt . Errorf ( "removing directory marker failed: %w" , err )
2022-09-26 15:43:40 +08:00
}
}
2019-08-15 23:26:16 +08:00
if bucket == "" || directory != "" {
2015-11-07 23:31:04 +08:00
return nil
}
2019-08-15 23:26:16 +08:00
return f . cache . Remove ( bucket , func ( ) error {
return f . pacer . Call ( func ( ) ( bool , error ) {
2023-03-12 16:59:21 +08:00
deleteBucket := f . svc . Buckets . Delete ( bucket ) . Context ( ctx )
if f . opt . UserProject != "" {
deleteBucket = deleteBucket . UserProject ( f . opt . UserProject )
}
err = deleteBucket . Do ( )
2021-03-11 22:44:01 +08:00
return shouldRetry ( ctx , err )
2019-08-15 23:26:16 +08:00
} )
2018-05-09 21:27:21 +08:00
} )
2014-07-14 00:54:03 +08:00
}
2015-09-23 01:47:16 +08:00
// Precision returns the precision
2015-11-07 19:14:46 +08:00
func ( f * Fs ) Precision ( ) time . Duration {
2014-07-14 00:54:03 +08:00
return time . Nanosecond
}
2020-10-14 05:43:40 +08:00
// Copy src to this remote using server-side copy operations.
2015-02-15 02:48:08 +08:00
//
2022-08-05 23:35:41 +08:00
// This is stored with the remote path given.
2015-02-15 02:48:08 +08:00
//
2022-08-05 23:35:41 +08:00
// It returns the destination Object and a possible error.
2015-02-15 02:48:08 +08:00
//
// Will only be called if src.Fs().Name() == f.Name()
//
// If it isn't possible then return fs.ErrorCantCopy
2019-06-17 16:34:30 +08:00
func ( f * Fs ) Copy ( ctx context . Context , src fs . Object , remote string ) ( fs . Object , error ) {
2019-08-15 23:26:16 +08:00
dstBucket , dstPath := f . split ( remote )
2023-04-27 00:53:48 +08:00
err := f . mkdirParent ( ctx , remote )
2017-06-29 04:14:53 +08:00
if err != nil {
return nil , err
}
2015-11-07 19:14:46 +08:00
srcObj , ok := src . ( * Object )
2015-02-15 02:48:08 +08:00
if ! ok {
2017-02-09 19:01:20 +08:00
fs . Debugf ( src , "Can't copy - not same remote type" )
2015-02-15 02:48:08 +08:00
return nil , fs . ErrorCantCopy
}
2019-08-15 23:26:16 +08:00
srcBucket , srcPath := srcObj . split ( )
2015-02-15 02:48:08 +08:00
2015-11-07 19:14:46 +08:00
// Temporary Object under construction
dstObj := & Object {
fs : f ,
remote : remote ,
}
2015-02-15 02:48:08 +08:00
2020-12-01 00:18:41 +08:00
rewriteRequest := f . svc . Objects . Rewrite ( srcBucket , srcPath , dstBucket , dstPath , nil )
if ! f . opt . BucketPolicyOnly {
rewriteRequest . DestinationPredefinedAcl ( f . opt . ObjectACL )
}
var rewriteResponse * storage . RewriteResponse
for {
err = f . pacer . Call ( func ( ) ( bool , error ) {
2023-03-12 16:59:21 +08:00
rewriteRequest = rewriteRequest . Context ( ctx )
if f . opt . UserProject != "" {
rewriteRequest . UserProject ( f . opt . UserProject )
}
rewriteResponse , err = rewriteRequest . Do ( )
2021-03-11 22:44:01 +08:00
return shouldRetry ( ctx , err )
2020-12-01 00:18:41 +08:00
} )
if err != nil {
return nil , err
2019-08-29 22:19:02 +08:00
}
2020-12-01 00:18:41 +08:00
if rewriteResponse . Done {
break
}
rewriteRequest . RewriteToken ( rewriteResponse . RewriteToken )
fs . Debugf ( dstObj , "Continuing rewrite %d bytes done" , rewriteResponse . TotalBytesRewritten )
2015-02-15 02:48:08 +08:00
}
// Set the metadata for the new object while we have it
2020-12-01 00:18:41 +08:00
dstObj . setMetaData ( rewriteResponse . Resource )
2015-02-15 02:48:08 +08:00
return dstObj , nil
}
2016-01-11 20:39:33 +08:00
// Hashes returns the supported hash sets.
2018-01-13 00:30:54 +08:00
func ( f * Fs ) Hashes ( ) hash . Set {
2018-01-19 04:27:52 +08:00
return hash . Set ( hash . MD5 )
2016-01-11 20:39:33 +08:00
}
2014-07-14 00:54:03 +08:00
// ------------------------------------------------------------
2015-09-23 01:47:16 +08:00
// Fs returns the parent Fs
2016-02-18 19:35:25 +08:00
func ( o * Object ) Fs ( ) fs . Info {
2015-11-07 19:14:46 +08:00
return o . fs
2014-07-14 00:54:03 +08:00
}
// Return a string version
2015-11-07 19:14:46 +08:00
func ( o * Object ) String ( ) string {
2014-07-14 00:54:03 +08:00
if o == nil {
return "<nil>"
}
return o . remote
}
2015-09-23 01:47:16 +08:00
// Remote returns the remote path
2015-11-07 19:14:46 +08:00
func ( o * Object ) Remote ( ) string {
2014-07-14 00:54:03 +08:00
return o . remote
}
2016-01-11 20:39:33 +08:00
// Hash returns the Md5sum of an object returning a lowercase hex string
2019-06-17 16:34:30 +08:00
func ( o * Object ) Hash ( ctx context . Context , t hash . Type ) ( string , error ) {
2018-01-19 04:27:52 +08:00
if t != hash . MD5 {
return "" , hash . ErrUnsupported
2016-01-11 20:39:33 +08:00
}
2014-07-14 00:54:03 +08:00
return o . md5sum , nil
}
// Size returns the size of an object in bytes
2015-11-07 19:14:46 +08:00
func ( o * Object ) Size ( ) int64 {
2014-07-14 00:54:03 +08:00
return o . bytes
}
// setMetaData sets the fs data from a storage.Object
2015-11-07 19:14:46 +08:00
func ( o * Object ) setMetaData ( info * storage . Object ) {
2014-07-14 00:54:03 +08:00
o . url = info . MediaLink
o . bytes = int64 ( info . Size )
2016-09-22 05:13:24 +08:00
o . mimeType = info . ContentType
2022-03-31 22:41:08 +08:00
o . gzipped = info . ContentEncoding == "gzip"
2014-07-14 00:54:03 +08:00
// Read md5sum
md5sumData , err := base64 . StdEncoding . DecodeString ( info . Md5Hash )
if err != nil {
2017-02-09 19:01:20 +08:00
fs . Logf ( o , "Bad MD5 decode: %v" , err )
2014-07-14 00:54:03 +08:00
} else {
o . md5sum = hex . EncodeToString ( md5sumData )
}
// read mtime out of metadata if available
mtimeString , ok := info . Metadata [ metaMtime ]
if ok {
2021-05-21 23:01:32 +08:00
modTime , err := time . Parse ( timeFormat , mtimeString )
2014-07-14 00:54:03 +08:00
if err == nil {
o . modTime = modTime
return
}
2017-02-09 19:01:20 +08:00
fs . Debugf ( o , "Failed to read mtime from metadata: %s" , err )
2014-07-14 00:54:03 +08:00
}
2021-05-21 17:11:43 +08:00
// Fallback to GSUtil mtime
mtimeGsutilString , ok := info . Metadata [ metaMtimeGsutil ]
if ok {
unixTimeSec , err := strconv . ParseInt ( mtimeGsutilString , 10 , 64 )
if err == nil {
o . modTime = time . Unix ( unixTimeSec , 0 )
return
}
fs . Debugf ( o , "Failed to read GSUtil mtime from metadata: %s" , err )
}
2014-07-14 00:54:03 +08:00
// Fallback to the Updated time
2021-05-21 23:01:32 +08:00
modTime , err := time . Parse ( timeFormat , info . Updated )
2014-07-14 00:54:03 +08:00
if err != nil {
2017-02-09 19:01:20 +08:00
fs . Logf ( o , "Bad time decode: %v" , err )
2014-07-14 00:54:03 +08:00
} else {
o . modTime = modTime
}
2022-03-31 22:41:08 +08:00
// If gunzipping then size and md5sum are unknown
2022-07-10 00:31:12 +08:00
if o . gzipped && o . fs . opt . Decompress {
2022-03-31 22:41:08 +08:00
o . bytes = - 1
o . md5sum = ""
}
2014-07-14 00:54:03 +08:00
}
2019-08-29 22:19:02 +08:00
// readObjectInfo reads the definition for an object
2019-09-06 20:50:36 +08:00
func ( o * Object ) readObjectInfo ( ctx context . Context ) ( object * storage . Object , err error ) {
2019-08-15 23:26:16 +08:00
bucket , bucketPath := o . split ( )
2023-04-27 00:53:48 +08:00
return o . fs . readObjectInfo ( ctx , bucket , bucketPath )
}
// readObjectInfo reads the definition for an object
func ( f * Fs ) readObjectInfo ( ctx context . Context , bucket , bucketPath string ) ( object * storage . Object , err error ) {
err = f . pacer . Call ( func ( ) ( bool , error ) {
get := f . svc . Objects . Get ( bucket , bucketPath ) . Context ( ctx )
if f . opt . UserProject != "" {
get = get . UserProject ( f . opt . UserProject )
2023-03-12 16:59:21 +08:00
}
object , err = get . Do ( )
2021-03-11 22:44:01 +08:00
return shouldRetry ( ctx , err )
2018-05-09 21:27:21 +08:00
} )
2014-07-14 00:54:03 +08:00
if err != nil {
2016-06-26 04:23:20 +08:00
if gErr , ok := err . ( * googleapi . Error ) ; ok {
if gErr . Code == http . StatusNotFound {
2019-08-29 22:19:02 +08:00
return nil , fs . ErrorObjectNotFound
2016-06-26 04:23:20 +08:00
}
}
2019-08-29 22:19:02 +08:00
return nil , err
}
return object , nil
}
// readMetaData gets the metadata if it hasn't already been fetched
//
// it also sets the info
2019-09-06 20:50:36 +08:00
func ( o * Object ) readMetaData ( ctx context . Context ) ( err error ) {
2019-08-29 22:19:02 +08:00
if ! o . modTime . IsZero ( ) {
return nil
}
2019-09-06 20:50:36 +08:00
object , err := o . readObjectInfo ( ctx )
2019-08-29 22:19:02 +08:00
if err != nil {
2014-07-14 00:54:03 +08:00
return err
}
o . setMetaData ( object )
return nil
}
// ModTime returns the modification time of the object
//
// It attempts to read the objects mtime and if that isn't present the
// LastModified returned in the http headers
2019-06-17 16:34:30 +08:00
func ( o * Object ) ModTime ( ctx context . Context ) time . Time {
2019-09-06 20:50:36 +08:00
err := o . readMetaData ( ctx )
2014-07-14 00:54:03 +08:00
if err != nil {
2017-02-09 19:01:20 +08:00
// fs.Logf(o, "Failed to read metadata: %v", err)
2014-07-14 00:54:03 +08:00
return time . Now ( )
}
return o . modTime
}
// Returns metadata for an object
func metadataFromModTime ( modTime time . Time ) map [ string ] string {
metadata := make ( map [ string ] string , 1 )
2021-05-21 23:01:32 +08:00
metadata [ metaMtime ] = modTime . Format ( timeFormat )
2021-05-21 17:11:43 +08:00
metadata [ metaMtimeGsutil ] = strconv . FormatInt ( modTime . Unix ( ) , 10 )
2014-07-14 00:54:03 +08:00
return metadata
}
2015-09-23 01:47:16 +08:00
// SetModTime sets the modification time of the local fs object
2019-06-17 16:34:30 +08:00
func ( o * Object ) SetModTime ( ctx context . Context , modTime time . Time ) ( err error ) {
2019-08-29 22:19:02 +08:00
// read the complete existing object first
2019-09-06 20:50:36 +08:00
object , err := o . readObjectInfo ( ctx )
2019-08-29 22:19:02 +08:00
if err != nil {
return err
2014-07-14 00:54:03 +08:00
}
2019-08-29 22:19:02 +08:00
// Add the mtime to the existing metadata
if object . Metadata == nil {
object . Metadata = make ( map [ string ] string , 1 )
}
2021-05-21 23:01:32 +08:00
object . Metadata [ metaMtime ] = modTime . Format ( timeFormat )
2021-05-21 17:11:43 +08:00
object . Metadata [ metaMtimeGsutil ] = strconv . FormatInt ( modTime . Unix ( ) , 10 )
2019-08-29 22:19:02 +08:00
// Copy the object to itself to update the metadata
// Using PATCH requires too many permissions
bucket , bucketPath := o . split ( )
2018-05-09 21:27:21 +08:00
var newObject * storage . Object
err = o . fs . pacer . Call ( func ( ) ( bool , error ) {
2019-08-29 22:19:02 +08:00
copyObject := o . fs . svc . Objects . Copy ( bucket , bucketPath , bucket , bucketPath , object )
if ! o . fs . opt . BucketPolicyOnly {
copyObject . DestinationPredefinedAcl ( o . fs . opt . ObjectACL )
}
2023-03-12 16:59:21 +08:00
copyObject = copyObject . Context ( ctx )
if o . fs . opt . UserProject != "" {
copyObject = copyObject . UserProject ( o . fs . opt . UserProject )
}
newObject , err = copyObject . Do ( )
2021-03-11 22:44:01 +08:00
return shouldRetry ( ctx , err )
2018-05-09 21:27:21 +08:00
} )
2014-07-14 00:54:03 +08:00
if err != nil {
2016-03-22 23:07:10 +08:00
return err
2014-07-14 00:54:03 +08:00
}
2014-07-29 03:07:02 +08:00
o . setMetaData ( newObject )
2016-03-22 23:07:10 +08:00
return nil
2014-07-14 00:54:03 +08:00
}
2015-09-23 01:47:16 +08:00
// Storable returns a boolean as to whether this object is storable
2015-11-07 19:14:46 +08:00
func ( o * Object ) Storable ( ) bool {
2014-07-14 00:54:03 +08:00
return true
}
// Open an object for read
2019-06-17 16:34:30 +08:00
func ( o * Object ) Open ( ctx context . Context , options ... fs . OpenOption ) ( in io . ReadCloser , err error ) {
2023-11-03 19:24:17 +08:00
url := o . url
2023-03-12 16:59:21 +08:00
if o . fs . opt . UserProject != "" {
2023-11-03 19:24:17 +08:00
url += "&userProject=" + o . fs . opt . UserProject
2023-03-12 16:59:21 +08:00
}
2023-11-03 19:24:17 +08:00
req , err := http . NewRequestWithContext ( ctx , "GET" , url , nil )
2014-07-15 18:18:43 +08:00
if err != nil {
return nil , err
}
2019-08-06 22:18:08 +08:00
fs . FixRangeOption ( options , o . bytes )
2022-07-10 00:31:12 +08:00
if o . gzipped && ! o . fs . opt . Decompress {
2022-03-31 22:41:08 +08:00
// Allow files which are stored on the cloud storage system
// compressed to be downloaded without being decompressed. Note
// that setting this here overrides the automatic decompression
// in the Transport.
//
// See: https://cloud.google.com/storage/docs/transcoding
req . Header . Set ( "Accept-Encoding" , "gzip" )
2022-07-10 00:31:12 +08:00
o . fs . warnCompressed . Do ( func ( ) {
fs . Logf ( o , "Not decompressing 'Content-Encoding: gzip' compressed file. Use --gcs-decompress to override" )
} )
2022-03-31 22:41:08 +08:00
}
2016-09-10 18:29:57 +08:00
fs . OpenOptionAddHTTPHeaders ( req . Header , options )
2018-05-09 21:27:21 +08:00
var res * http . Response
err = o . fs . pacer . Call ( func ( ) ( bool , error ) {
res , err = o . fs . client . Do ( req )
if err == nil {
err = googleapi . CheckResponse ( res )
if err != nil {
_ = res . Body . Close ( ) // ignore error
}
}
2021-03-11 22:44:01 +08:00
return shouldRetry ( ctx , err )
2018-05-09 21:27:21 +08:00
} )
2014-07-14 00:54:03 +08:00
if err != nil {
return nil , err
}
2016-09-10 18:29:57 +08:00
_ , isRanging := req . Header [ "Range" ]
if ! ( res . StatusCode == http . StatusOK || ( isRanging && res . StatusCode == http . StatusPartialContent ) ) {
2014-07-26 01:19:49 +08:00
_ = res . Body . Close ( ) // ignore error
2021-11-04 18:12:57 +08:00
return nil , fmt . Errorf ( "bad response: %d: %s" , res . StatusCode , res . Status )
2014-07-14 00:54:03 +08:00
}
return res . Body , nil
}
// Update the object with the contents of the io.Reader, modTime and size
//
// The new object may have been created if an error is returned
2023-04-27 00:53:48 +08:00
func ( o * Object ) Update ( ctx context . Context , in io . Reader , src fs . ObjectInfo , options ... fs . OpenOption ) ( err error ) {
2019-08-15 23:26:16 +08:00
bucket , bucketPath := o . split ( )
2023-04-27 00:53:48 +08:00
// Create parent dir/bucket if not saving directory marker
if ! strings . HasSuffix ( o . remote , "/" ) {
err = o . fs . mkdirParent ( ctx , o . remote )
if err != nil {
return err
}
2017-06-07 21:16:50 +08:00
}
2019-06-17 16:34:30 +08:00
modTime := src . ModTime ( ctx )
2016-02-18 19:35:25 +08:00
2014-07-14 00:54:03 +08:00
object := storage . Object {
2019-08-15 23:26:16 +08:00
Bucket : bucket ,
Name : bucketPath ,
2019-06-17 16:34:30 +08:00
ContentType : fs . MimeType ( ctx , src ) ,
2014-07-14 19:44:31 +08:00
Metadata : metadataFromModTime ( modTime ) ,
2014-07-14 00:54:03 +08:00
}
2020-05-02 16:15:28 +08:00
// Apply upload options
for _ , option := range options {
key , value := option . Header ( )
lowerKey := strings . ToLower ( key )
switch lowerKey {
case "" :
// ignore
case "cache-control" :
object . CacheControl = value
case "content-disposition" :
object . ContentDisposition = value
case "content-encoding" :
object . ContentEncoding = value
case "content-language" :
object . ContentLanguage = value
case "content-type" :
object . ContentType = value
2020-12-03 16:52:12 +08:00
case "x-goog-storage-class" :
object . StorageClass = value
2020-05-02 16:15:28 +08:00
default :
const googMetaPrefix = "x-goog-meta-"
if strings . HasPrefix ( lowerKey , googMetaPrefix ) {
metaKey := lowerKey [ len ( googMetaPrefix ) : ]
object . Metadata [ metaKey ] = value
} else {
fs . Errorf ( o , "Don't know how to set key %q on upload" , key )
}
}
}
2018-05-09 21:27:21 +08:00
var newObject * storage . Object
err = o . fs . pacer . CallNoRetry ( func ( ) ( bool , error ) {
2019-08-15 23:26:16 +08:00
insertObject := o . fs . svc . Objects . Insert ( bucket , & object ) . Media ( in , googleapi . ContentType ( "" ) ) . Name ( object . Name )
2019-03-04 22:52:54 +08:00
if ! o . fs . opt . BucketPolicyOnly {
insertObject . PredefinedAcl ( o . fs . opt . ObjectACL )
}
2023-03-12 16:59:21 +08:00
insertObject = insertObject . Context ( ctx )
if o . fs . opt . UserProject != "" {
insertObject = insertObject . UserProject ( o . fs . opt . UserProject )
}
newObject , err = insertObject . Do ( )
2021-03-11 22:44:01 +08:00
return shouldRetry ( ctx , err )
2018-05-09 21:27:21 +08:00
} )
2014-07-22 04:25:46 +08:00
if err != nil {
return err
}
2014-07-16 19:12:36 +08:00
// Set the metadata for the new object while we have it
o . setMetaData ( newObject )
2014-07-22 04:25:46 +08:00
return nil
2014-07-14 00:54:03 +08:00
}
// Remove an object
2019-06-17 16:34:30 +08:00
func ( o * Object ) Remove ( ctx context . Context ) ( err error ) {
2019-08-15 23:26:16 +08:00
bucket , bucketPath := o . split ( )
2018-05-09 21:27:21 +08:00
err = o . fs . pacer . Call ( func ( ) ( bool , error ) {
2023-03-12 16:59:21 +08:00
deleteBucket := o . fs . svc . Objects . Delete ( bucket , bucketPath ) . Context ( ctx )
if o . fs . opt . UserProject != "" {
deleteBucket = deleteBucket . UserProject ( o . fs . opt . UserProject )
}
err = deleteBucket . Do ( )
2021-03-11 22:44:01 +08:00
return shouldRetry ( ctx , err )
2018-05-09 21:27:21 +08:00
} )
return err
2014-07-14 00:54:03 +08:00
}
2016-09-22 05:13:24 +08:00
// MimeType of an Object if known, "" otherwise
2019-06-17 16:34:30 +08:00
func ( o * Object ) MimeType ( ctx context . Context ) string {
2016-09-22 05:13:24 +08:00
return o . mimeType
}
2014-07-14 00:54:03 +08:00
// Check the interfaces are satisfied
2015-11-07 19:14:46 +08:00
var (
2017-09-17 04:46:02 +08:00
_ fs . Fs = & Fs { }
_ fs . Copier = & Fs { }
_ fs . PutStreamer = & Fs { }
_ fs . ListRer = & Fs { }
_ fs . Object = & Object { }
_ fs . MimeTyper = & Object { }
2015-11-07 19:14:46 +08:00
)