Skip to content

Commit 0576e0f

Browse files
committed
io: support aliyun oss backend
Signed-off-by: divinerapier <sihao.fang@outlook.com>
1 parent c1ffe04 commit 0576e0f

File tree

6 files changed

+216
-0
lines changed

6 files changed

+216
-0
lines changed

catalog/catalog.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323
"errors"
2424
"net/url"
2525

26+
"github.com/aliyun/alibabacloud-oss-go-sdk-v2/oss"
2627
"github.com/apache/iceberg-go"
2728
"github.com/apache/iceberg-go/table"
2829
"github.com/aws/aws-sdk-go-v2/aws"
@@ -32,6 +33,18 @@ type CatalogType string
3233

3334
type AwsProperties map[string]string
3435

36+
// OSSConfig contains configuration for accessing OSS object storage
37+
type OSSConfig struct {
38+
// Endpoint specifies the OSS service endpoint URL
39+
Endpoint string
40+
// AccessKey is the access key ID for OSS authentication
41+
AccessKey string
42+
// SecretKey is the secret access key for OSS authentication
43+
SecretKey string
44+
// SignatureVersion is the signature version for OSS authentication
45+
SignatureVersion oss.SignatureVersionType
46+
}
47+
3548
const (
3649
REST CatalogType = "rest"
3750
Hive CatalogType = "hive"
@@ -122,12 +135,21 @@ func WithPrefix(prefix string) Option[RestCatalog] {
122135
}
123136
}
124137

138+
// WithOSSConfig sets the OSS configuration for the catalog.
139+
func WithOSSConfig(cfg OSSConfig) Option[RestCatalog] {
140+
return func(o *options) {
141+
o.ossConfig = cfg
142+
}
143+
}
144+
125145
type Option[T GlueCatalog | RestCatalog] func(*options)
126146

127147
type options struct {
128148
awsConfig aws.Config
129149
awsProperties AwsProperties
130150

151+
ossConfig OSSConfig
152+
131153
tlsConfig *tls.Config
132154
credential string
133155
oauthToken string

catalog/rest.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import (
2929
"maps"
3030
"net/http"
3131
"net/url"
32+
"strconv"
3233
"strings"
3334
"time"
3435

@@ -56,6 +57,11 @@ const (
5657
keyRestSigV4Region = "rest.signing-region"
5758
keyRestSigV4Service = "rest.signing-name"
5859
keyAuthUrl = "rest.authorization-url"
60+
61+
keyOSSAccessKey = "client.oss-access-key"
62+
keyOSSSecretKey = "client.oss-secret-key"
63+
keyOSSEndpoint = "client.oss-endpoint"
64+
keyOSSSignatureVersion = "client.oss-signature-version"
5965
)
6066

6167
var (
@@ -356,6 +362,12 @@ func toProps(o *options) iceberg.Properties {
356362
if o.authUri != nil {
357363
setIf(keyAuthUrl, o.authUri.String())
358364
}
365+
366+
setIf(keyOSSAccessKey, o.ossConfig.AccessKey)
367+
setIf(keyOSSSecretKey, o.ossConfig.SecretKey)
368+
setIf(keyOSSEndpoint, o.ossConfig.Endpoint)
369+
// Convert OSS signature version from enum to string representation
370+
setIf(keyOSSSignatureVersion, strconv.FormatInt(int64(o.ossConfig.SignatureVersion), 10))
359371
return props
360372
}
361373

@@ -515,6 +527,7 @@ func (r *RestCatalog) fetchConfig(opts *options) (*options, error) {
515527

516528
o := fromProps(cfg)
517529
o.awsConfig = opts.awsConfig
530+
o.ossConfig = opts.ossConfig
518531
o.tlsConfig = opts.tlsConfig
519532

520533
if uri, ok := cfg["uri"]; ok {

go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ go 1.23
2222
toolchain go1.23.2
2323

2424
require (
25+
github.com/aliyun/alibabacloud-oss-go-sdk-v2 v1.1.3
2526
github.com/apache/arrow-go/v18 v18.0.1-0.20241029153821-f0c5d9939d3f
2627
github.com/aws/aws-sdk-go-v2 v1.32.5
2728
github.com/aws/aws-sdk-go-v2/config v1.28.5
@@ -97,6 +98,7 @@ require (
9798
golang.org/x/sys v0.26.0 // indirect
9899
golang.org/x/term v0.25.0 // indirect
99100
golang.org/x/text v0.19.0 // indirect
101+
golang.org/x/time v0.4.0 // indirect
100102
golang.org/x/tools v0.26.0 // indirect
101103
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect
102104
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect

go.sum

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ github.com/alecthomas/participle/v2 v2.1.0 h1:z7dElHRrOEEq45F2TG5cbQihMtNTv8vwld
2323
github.com/alecthomas/participle/v2 v2.1.0/go.mod h1:Y1+hAs8DHPmc3YUFzqllV+eSQ9ljPTk0ZkPMtEdAx2c=
2424
github.com/alecthomas/repr v0.2.0 h1:HAzS41CIzNW5syS8Mf9UwXhNH1J9aix/BvDRf1Ml2Yk=
2525
github.com/alecthomas/repr v0.2.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
26+
github.com/aliyun/alibabacloud-oss-go-sdk-v2 v1.1.3 h1:grJyLSdRJtfxKKhCTWSeJhnOQsp2WoLNdK8XA5FE9oo=
27+
github.com/aliyun/alibabacloud-oss-go-sdk-v2 v1.1.3/go.mod h1:FTzydeQVmR24FI0D6XWUOMKckjXehM/jgMn1xC+DA9M=
2628
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
2729
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
2830
github.com/apache/arrow-go/v18 v18.0.1-0.20241029153821-f0c5d9939d3f h1:k14GhTGJuvq27vRgLxf4iuufzLt7GeN3UOytJmU7W/A=
@@ -243,6 +245,8 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
243245
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
244246
golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
245247
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
248+
golang.org/x/time v0.4.0 h1:Z81tqI5ddIoXDPvVQ7/7CC9TnLM7ubaFG2qXYd5BbYY=
249+
golang.org/x/time v0.4.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
246250
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
247251
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
248252
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=

io/io.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,8 @@ func inferFileIOFromSchema(path string, props map[string]string) (IO, error) {
215215
switch parsed.Scheme {
216216
case "s3", "s3a", "s3n":
217217
return createS3FileIO(parsed, props)
218+
case "oss":
219+
return createOSSFileIO(parsed, props)
218220
case "file", "":
219221
return LocalFS{}, nil
220222
default:

io/oss.go

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
// Licensed to the Apache Software Foundation (ASF) under one
2+
// or more contributor license agreements. See the NOTICE file
3+
// distributed with this work for additional information
4+
// regarding copyright ownership. The ASF licenses this file
5+
// to you under the Apache License, Version 2.0 (the
6+
// "License"); you may not use this file except in compliance
7+
// with the License. You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
18+
package io
19+
20+
import (
21+
"context"
22+
"fmt"
23+
"io"
24+
"io/fs"
25+
"net/url"
26+
"os"
27+
"strings"
28+
"sync"
29+
"time"
30+
31+
"github.com/aliyun/alibabacloud-oss-go-sdk-v2/oss"
32+
"github.com/aliyun/alibabacloud-oss-go-sdk-v2/oss/credentials"
33+
)
34+
35+
// Constants for OSS configuration options
36+
const (
37+
OSSAccessKey = "client.oss-access-key"
38+
OSSSecretKey = "client.oss-secret-key"
39+
OSSEndpoint = "client.oss-endpoint"
40+
OSSSignatureVersion = "client.oss-signature-version"
41+
)
42+
43+
func createOSSFileIO(parsed *url.URL, props map[string]string) (IO, error) {
44+
endpoint, ok := props[OSSEndpoint]
45+
if !ok {
46+
endpoint = os.Getenv("OSS_ENDPOINT")
47+
}
48+
if endpoint == "" {
49+
return nil, fmt.Errorf("oss endpoint must be specified")
50+
}
51+
52+
accessKey := props[OSSAccessKey]
53+
secretKey := props[OSSSecretKey]
54+
55+
provider := credentials.NewStaticCredentialsProvider(accessKey, secretKey)
56+
57+
cfg := oss.LoadDefaultConfig().
58+
WithRetryMaxAttempts(3).
59+
WithCredentialsProvider(provider).
60+
WithEndpoint(endpoint).
61+
WithSignatureVersion(parseSignatureVersion(props[OSSSignatureVersion])).
62+
WithUsePathStyle(true).
63+
WithConnectTimeout(10 * time.Second).
64+
WithReadWriteTimeout(time.Minute)
65+
66+
client := oss.NewClient(cfg)
67+
68+
ossFS := &ossFS{
69+
client: client,
70+
bucket: parsed.Host,
71+
}
72+
73+
preprocess := func(n string) string {
74+
_, after, found := strings.Cut(n, "://")
75+
if found {
76+
n = after
77+
}
78+
return strings.TrimPrefix(n, parsed.Host)
79+
}
80+
81+
return FSPreProcName(ossFS, preprocess), nil
82+
}
83+
84+
// parseSignatureVersion converts string version to oss.SignatureVersionType
85+
// "0" -> 0 (v1)
86+
// "1" -> 1 (v4)
87+
// defaults to 1 (v4) for unknown values
88+
func parseSignatureVersion(version string) oss.SignatureVersionType {
89+
switch version {
90+
case "0": // v1
91+
return 0
92+
case "1": // v4
93+
return 1
94+
default:
95+
return 1
96+
}
97+
}
98+
99+
type ossFS struct {
100+
client *oss.Client
101+
bucket string
102+
}
103+
104+
// Open implements fs.FS
105+
func (o *ossFS) Open(name string) (fs.File, error) {
106+
if !fs.ValidPath(name) {
107+
return nil, &os.PathError{Op: "open", Path: name, Err: os.ErrInvalid}
108+
}
109+
if name == "." {
110+
return &ossFile{
111+
name: name,
112+
}, nil
113+
}
114+
name = strings.TrimPrefix(name, "/")
115+
116+
file, err := o.client.OpenFile(context.Background(), o.bucket, name)
117+
if err != nil {
118+
return nil, err
119+
}
120+
121+
return &ossFile{
122+
file: file,
123+
name: name,
124+
}, nil
125+
}
126+
127+
type ossFile struct {
128+
mutex sync.Mutex
129+
file *oss.ReadOnlyFile
130+
name string
131+
}
132+
133+
// Read implements io.Reader
134+
func (f *ossFile) Read(p []byte) (int, error) {
135+
f.mutex.Lock()
136+
defer f.mutex.Unlock()
137+
138+
return f.file.Read(p)
139+
}
140+
141+
// Seek implements io.Seeker
142+
func (f *ossFile) Seek(offset int64, whence int) (int64, error) {
143+
f.mutex.Lock()
144+
defer f.mutex.Unlock()
145+
146+
return f.file.Seek(offset, whence)
147+
}
148+
149+
// Close implements io.Closer
150+
func (f *ossFile) Close() error {
151+
f.mutex.Lock()
152+
defer f.mutex.Unlock()
153+
154+
return f.file.Close()
155+
}
156+
157+
// ReadAt implements io.ReaderAt
158+
func (f *ossFile) ReadAt(p []byte, off int64) (n int, err error) {
159+
f.mutex.Lock()
160+
defer f.mutex.Unlock()
161+
162+
if _, err := f.file.Seek(off, io.SeekStart); err != nil {
163+
return 0, err
164+
}
165+
return f.file.Read(p)
166+
}
167+
168+
func (f *ossFile) Stat() (fs.FileInfo, error) {
169+
f.mutex.Lock()
170+
defer f.mutex.Unlock()
171+
172+
return f.file.Stat()
173+
}

0 commit comments

Comments
 (0)