go-duck-cli 1.0.9 → 1.1.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -15
- package/generators/ai_docs.js +130 -0
- package/generators/broker.js +63 -0
- package/generators/config.js +149 -7
- package/generators/devops.js +210 -43
- package/generators/docs.js +23 -4
- package/generators/elasticsearch.js +263 -0
- package/generators/kratos.js +229 -41
- package/generators/metering.js +280 -48
- package/generators/migrations.js +92 -198
- package/generators/mqtt.js +2 -39
- package/generators/multitenancy.js +274 -71
- package/generators/nats.js +39 -0
- package/generators/outbox.js +171 -0
- package/generators/postgrest.js +7 -3
- package/generators/postman.js +405 -0
- package/generators/repository.js +27 -0
- package/generators/router.js +27 -0
- package/generators/security.js +95 -14
- package/generators/serverless.js +147 -0
- package/generators/storage.js +589 -0
- package/generators/swagger.js +84 -60
- package/generators/telemetry.js +23 -32
- package/generators/websocket.js +55 -21
- package/index.js +493 -116
- package/package.json +6 -4
- package/parser/gdl.js +163 -24
- package/templates/docs/index.html.hbs +5 -5
- package/templates/docs/layout.hbs +221 -62
- package/templates/docs/pages/audit.hbs +83 -35
- package/templates/docs/pages/cli.hbs +18 -0
- package/templates/docs/pages/configuration.hbs +241 -0
- package/templates/docs/pages/datadog.hbs +46 -0
- package/templates/docs/pages/elasticsearch.hbs +121 -0
- package/templates/docs/pages/federation.hbs +241 -0
- package/templates/docs/pages/gdl-advanced.hbs +91 -0
- package/templates/docs/pages/gdl-annotations.hbs +137 -0
- package/templates/docs/pages/gdl-entities.hbs +134 -0
- package/templates/docs/pages/gdl-relationships.hbs +80 -0
- package/templates/docs/pages/gdl.hbs +60 -204
- package/templates/docs/pages/graphql.hbs +58 -44
- package/templates/docs/pages/grpc.hbs +53 -90
- package/templates/docs/pages/hybrid-store.hbs +127 -0
- package/templates/docs/pages/index.hbs +418 -149
- package/templates/docs/pages/keycloak.hbs +43 -0
- package/templates/docs/pages/legend.hbs +116 -0
- package/templates/docs/pages/mosquitto.hbs +39 -0
- package/templates/docs/pages/multitenancy.hbs +139 -71
- package/templates/docs/pages/otel.hbs +40 -0
- package/templates/docs/pages/realtime.hbs +38 -12
- package/templates/docs/pages/redis.hbs +40 -0
- package/templates/docs/pages/rest.hbs +120 -202
- package/templates/docs/pages/saga.hbs +94 -0
- package/templates/docs/pages/security.hbs +150 -44
- package/templates/docs/pages/serverless.hbs +157 -0
- package/templates/docs/pages/storage.hbs +127 -0
- package/templates/docs/pages/wizard.hbs +683 -0
- package/templates/docs/triple_identity_registry.png +0 -0
- package/templates/go/controller.go.hbs +287 -283
- package/templates/go/entity.go.hbs +17 -15
- package/templates/go/main.go.hbs +47 -180
- package/templates/go/migrator.go.hbs +65 -0
- package/templates/go/router.go.hbs +272 -0
- package/templates/graphql/resolver.go.hbs +53 -34
- package/templates/graphql/schema.graphql.hbs +17 -5
- package/templates/kratos/service.go.hbs +169 -34
- package/templates/proto/entity.proto.hbs +10 -14
- package/test_nested.gdl +21 -0
- package/templates/docs/intro.mp4 +0 -0
- package/test_parser.js +0 -9
|
@@ -0,0 +1,589 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
|
|
5
|
+
export const generateStorageCode = async (config, outputDir, configSourceDir) => {
|
|
6
|
+
const storageDir = path.join(outputDir, 'internal/storage');
|
|
7
|
+
await fs.ensureDir(storageDir);
|
|
8
|
+
|
|
9
|
+
// 1. Manage GCS Credentials File (Local CLI Flow - for backward compatibility)
|
|
10
|
+
if (config.storage?.gcs?.enabled && config.storage.gcs['credentials-file']) {
|
|
11
|
+
const credsFile = config.storage.gcs['credentials-file'];
|
|
12
|
+
const sourcePath = path.join(configSourceDir, credsFile);
|
|
13
|
+
const destPath = path.join(outputDir, 'config', credsFile);
|
|
14
|
+
|
|
15
|
+
if (await fs.pathExists(sourcePath)) {
|
|
16
|
+
await fs.ensureDir(path.dirname(destPath));
|
|
17
|
+
await fs.copy(sourcePath, destPath);
|
|
18
|
+
console.log(chalk.gray(` Copied GCS credentials: ${credsFile}`));
|
|
19
|
+
} else {
|
|
20
|
+
console.warn(chalk.yellow(` ⚠️ GCS credentials file not found at ${sourcePath}`));
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const storageInterfaceGo = `package storage
|
|
25
|
+
|
|
26
|
+
import (
|
|
27
|
+
"context"
|
|
28
|
+
"time"
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
// StorageProvider defines the universal interface for object storage across multiple providers
|
|
32
|
+
type StorageProvider interface {
|
|
33
|
+
Upload(ctx context.Context, key string, data []byte) (string, error)
|
|
34
|
+
Download(ctx context.Context, key string) ([]byte, error)
|
|
35
|
+
Delete(ctx context.Context, key string) error
|
|
36
|
+
GetSignedURL(ctx context.Context, key string, expires time.Duration) (string, error)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
var Providers = make(map[string]StorageProvider)
|
|
40
|
+
`;
|
|
41
|
+
|
|
42
|
+
const bootstrapperGo = `package storage
|
|
43
|
+
|
|
44
|
+
import (
|
|
45
|
+
"fmt"
|
|
46
|
+
"io/ioutil"
|
|
47
|
+
"log"
|
|
48
|
+
"net/http"
|
|
49
|
+
"os"
|
|
50
|
+
"path/filepath"
|
|
51
|
+
"{{app_name}}/config"
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
// BootstrapCredentials pulls secret files (id_rsa, service.json) from GitHub before app start
|
|
55
|
+
func BootstrapCredentials(cfg *config.Config) error {
|
|
56
|
+
if !cfg.GoDuck.Storage.Bootstrap.Enabled {
|
|
57
|
+
return nil
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
log.Println("🚀 Starting Remote GitHub Credential Bootstrapping...")
|
|
61
|
+
|
|
62
|
+
client := &http.Client{}
|
|
63
|
+
b := cfg.GoDuck.Storage.Bootstrap
|
|
64
|
+
|
|
65
|
+
for _, filename := range b.Files {
|
|
66
|
+
// GitHub Raw Content URL
|
|
67
|
+
url := fmt.Sprintf("https://raw.githubusercontent.com/%s/%s/%s/%s", b.Owner, b.Repo, b.Branch, filename)
|
|
68
|
+
req, _ := http.NewRequest("GET", url, nil)
|
|
69
|
+
|
|
70
|
+
if b.Token != "" {
|
|
71
|
+
req.Header.Set("Authorization", "token "+b.Token)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
resp, err := client.Do(req)
|
|
75
|
+
if err != nil {
|
|
76
|
+
return fmt.Errorf("failed to fetch %s: %v", filename, err)
|
|
77
|
+
}
|
|
78
|
+
defer resp.Body.Close()
|
|
79
|
+
|
|
80
|
+
if resp.StatusCode != 200 {
|
|
81
|
+
return fmt.Errorf("GitHub returned %d for %s", resp.StatusCode, filename)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
body, _ := ioutil.ReadAll(resp.Body)
|
|
85
|
+
|
|
86
|
+
// Save to local config folder (K8s ephemeral storage)
|
|
87
|
+
dest := filepath.Join("config", filename)
|
|
88
|
+
os.MkdirAll("config", 0755)
|
|
89
|
+
|
|
90
|
+
if err := ioutil.WriteFile(dest, body, 0600); err != nil {
|
|
91
|
+
return fmt.Errorf("failed to write bootstrap file %s: %v", filename, err)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
log.Printf("✅ Successfully bootstrapped: %s", filename)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return nil
|
|
98
|
+
}
|
|
99
|
+
`;
|
|
100
|
+
|
|
101
|
+
const s3ProviderGo = `package storage
|
|
102
|
+
|
|
103
|
+
import (
|
|
104
|
+
"context"
|
|
105
|
+
"time"
|
|
106
|
+
"{{app_name}}/config"
|
|
107
|
+
s3vg2 "github.com/aws/aws-sdk-go-v2/service/s3"
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
type S3Provider struct {
|
|
111
|
+
client *s3vg2.Client
|
|
112
|
+
bucket string
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
func NewS3Provider(cfg config.Config) *S3Provider {
|
|
116
|
+
s3Cfg := cfg.GoDuck.Storage.S3
|
|
117
|
+
return &S3Provider{bucket: s3Cfg.Bucket}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
func (p *S3Provider) Upload(ctx context.Context, key string, data []byte) (string, error) {
|
|
121
|
+
return "s3://" + p.bucket + "/" + key, nil
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
func (p *S3Provider) Download(ctx context.Context, key string) ([]byte, error) {
|
|
125
|
+
return nil, nil
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
func (p *S3Provider) Delete(ctx context.Context, key string) error {
|
|
129
|
+
return nil
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
func (p *S3Provider) GetSignedURL(ctx context.Context, key string, expires time.Duration) (string, error) {
|
|
133
|
+
return "", nil
|
|
134
|
+
}
|
|
135
|
+
`;
|
|
136
|
+
|
|
137
|
+
const gcsProviderGo = `package storage
|
|
138
|
+
|
|
139
|
+
import (
|
|
140
|
+
"context"
|
|
141
|
+
"time"
|
|
142
|
+
"{{app_name}}/config"
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
type GCSProvider struct {
|
|
146
|
+
bucket string
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
func NewGCSProvider(cfg config.Config) *GCSProvider {
|
|
150
|
+
return &GCSProvider{bucket: cfg.GoDuck.Storage.GCS.Bucket}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
func (p *GCSProvider) Upload(ctx context.Context, key string, data []byte) (string, error) {
|
|
154
|
+
return "gs://" + p.bucket + "/" + key, nil
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
func (p *GCSProvider) Download(ctx context.Context, key string) ([]byte, error) {
|
|
158
|
+
return nil, nil
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
func (p *GCSProvider) Delete(ctx context.Context, key string) error {
|
|
162
|
+
return nil
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
func (p *GCSProvider) GetSignedURL(ctx context.Context, key string, expires time.Duration) (string, error) {
|
|
166
|
+
return "", nil
|
|
167
|
+
}
|
|
168
|
+
`;
|
|
169
|
+
|
|
170
|
+
const providerFactoryGo = `package storage
|
|
171
|
+
|
|
172
|
+
import (
|
|
173
|
+
"bytes"
|
|
174
|
+
"context"
|
|
175
|
+
"encoding/base64"
|
|
176
|
+
"encoding/json"
|
|
177
|
+
"fmt"
|
|
178
|
+
"io"
|
|
179
|
+
"net/http"
|
|
180
|
+
"os"
|
|
181
|
+
"path/filepath"
|
|
182
|
+
"time"
|
|
183
|
+
|
|
184
|
+
"golang.org/x/crypto/ssh"
|
|
185
|
+
"github.com/pkg/sftp"
|
|
186
|
+
"{{app_name}}/config"
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
func InitStorage(cfg *config.Config) error {
|
|
190
|
+
// 1. Fetch Remote Credentials if enabled
|
|
191
|
+
if err := BootstrapCredentials(cfg); err != nil {
|
|
192
|
+
return err
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if cfg.GoDuck.Storage.S3.Enabled {
|
|
196
|
+
Providers["s3"] = NewS3Provider(*cfg)
|
|
197
|
+
}
|
|
198
|
+
if cfg.GoDuck.Storage.GCS.Enabled {
|
|
199
|
+
Providers["gcs"] = NewGCSProvider(*cfg)
|
|
200
|
+
}
|
|
201
|
+
if cfg.GoDuck.Storage.MinIO.Enabled {
|
|
202
|
+
Providers["minio"] = NewS3Provider(*cfg)
|
|
203
|
+
}
|
|
204
|
+
if cfg.GoDuck.Storage.R2.Enabled {
|
|
205
|
+
Providers["r2"] = NewS3Provider(*cfg)
|
|
206
|
+
}
|
|
207
|
+
if cfg.GoDuck.Storage.Generic.Enabled {
|
|
208
|
+
Providers["generic"] = NewS3Provider(*cfg)
|
|
209
|
+
}
|
|
210
|
+
if cfg.GoDuck.Storage.SFTP.Enabled {
|
|
211
|
+
Providers["sftp"] = NewSFTPProvider(*cfg)
|
|
212
|
+
}
|
|
213
|
+
if cfg.GoDuck.Storage.GitHub.Enabled {
|
|
214
|
+
Providers["github"] = NewGitHubProvider(*cfg)
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return nil
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
type SFTPProvider struct {
|
|
221
|
+
host string
|
|
222
|
+
remotePath string
|
|
223
|
+
client *sftp.Client
|
|
224
|
+
sshClient *ssh.Client
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
func NewSFTPProvider(cfg config.Config) *SFTPProvider {
|
|
228
|
+
sftpCfg := cfg.GoDuck.Storage.SFTP
|
|
229
|
+
|
|
230
|
+
var authMethod ssh.AuthMethod
|
|
231
|
+
if sftpCfg.Password == "" && sftpCfg.KeyFile != "" {
|
|
232
|
+
keyPath := filepath.Join("config", sftpCfg.KeyFile) // Loaded from github bootstrap
|
|
233
|
+
if key, err := os.ReadFile(keyPath); err == nil {
|
|
234
|
+
if signer, parseErr := ssh.ParsePrivateKey(key); parseErr == nil {
|
|
235
|
+
authMethod = ssh.PublicKeys(signer)
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
} else if sftpCfg.Password != "" {
|
|
239
|
+
authMethod = ssh.Password(sftpCfg.Password)
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if authMethod == nil {
|
|
243
|
+
authMethod = ssh.Password("fallback")
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
clientConfig := &ssh.ClientConfig{
|
|
247
|
+
User: sftpCfg.Username,
|
|
248
|
+
Auth: []ssh.AuthMethod{authMethod},
|
|
249
|
+
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
|
|
250
|
+
Timeout: 5 * time.Second,
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
addr := fmt.Sprintf("%s:%d", sftpCfg.Host, sftpCfg.Port)
|
|
254
|
+
sshConn, err := ssh.Dial("tcp", addr, clientConfig)
|
|
255
|
+
var sftpClient *sftp.Client
|
|
256
|
+
if err == nil {
|
|
257
|
+
sftpClient, _ = sftp.NewClient(sshConn)
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return &SFTPProvider{
|
|
261
|
+
host: sftpCfg.Host,
|
|
262
|
+
remotePath: sftpCfg.RemotePath,
|
|
263
|
+
client: sftpClient,
|
|
264
|
+
sshClient: sshConn,
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
func (p *SFTPProvider) Upload(ctx context.Context, key string, data []byte) (string, error) {
|
|
269
|
+
if p.client == nil {
|
|
270
|
+
return "", fmt.Errorf("sftp client is not connected")
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Calculate absolute remote path
|
|
274
|
+
remotePath := key
|
|
275
|
+
if p.remotePath != "" {
|
|
276
|
+
remotePath = filepath.Join(p.remotePath, key)
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Ensure structural integrity
|
|
280
|
+
dir := filepath.Dir(remotePath)
|
|
281
|
+
p.client.MkdirAll(dir) // Automatically handles parent folders
|
|
282
|
+
|
|
283
|
+
f, err := p.client.Create(remotePath)
|
|
284
|
+
if err != nil {
|
|
285
|
+
return "", err
|
|
286
|
+
}
|
|
287
|
+
defer f.Close()
|
|
288
|
+
if _, err := f.Write(data); err != nil {
|
|
289
|
+
return "", err
|
|
290
|
+
}
|
|
291
|
+
return "sftp://" + p.host + "/" + key, nil
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
func (p *SFTPProvider) Download(ctx context.Context, key string) ([]byte, error) {
|
|
295
|
+
if p.client == nil {
|
|
296
|
+
return nil, fmt.Errorf("sftp client is not connected")
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
f, err := p.client.Open(key)
|
|
300
|
+
if err != nil {
|
|
301
|
+
return nil, err
|
|
302
|
+
}
|
|
303
|
+
defer f.Close()
|
|
304
|
+
|
|
305
|
+
return io.ReadAll(f)
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
func (p *SFTPProvider) Delete(ctx context.Context, key string) error {
|
|
309
|
+
return nil
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
func (p *SFTPProvider) GetSignedURL(ctx context.Context, key string, expires time.Duration) (string, error) {
|
|
313
|
+
return "", nil
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
type GitHubProvider struct {
|
|
317
|
+
owner string
|
|
318
|
+
repo string
|
|
319
|
+
branch string
|
|
320
|
+
token string
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
func NewGitHubProvider(cfg config.Config) *GitHubProvider {
|
|
324
|
+
ghCfg := cfg.GoDuck.Storage.GitHub
|
|
325
|
+
branch := ghCfg.Branch
|
|
326
|
+
if branch == "" {
|
|
327
|
+
branch = "main" // Default safeguard
|
|
328
|
+
}
|
|
329
|
+
return &GitHubProvider{
|
|
330
|
+
owner: ghCfg.Owner,
|
|
331
|
+
repo: ghCfg.Repo,
|
|
332
|
+
branch: branch,
|
|
333
|
+
token: ghCfg.Token,
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
type ghCommitPayload struct {
|
|
338
|
+
Message string \`json:"message"\`
|
|
339
|
+
Content string \`json:"content"\`
|
|
340
|
+
Branch string \`json:"branch"\`
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
func (p *GitHubProvider) Upload(ctx context.Context, key string, data []byte) (string, error) {
|
|
344
|
+
if p.owner == "" || p.repo == "" {
|
|
345
|
+
return "", fmt.Errorf("GitHub provider missing owner or repo in config")
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
url := fmt.Sprintf("https://api.github.com/repos/%s/%s/contents/%s", p.owner, p.repo, key)
|
|
349
|
+
|
|
350
|
+
payload := ghCommitPayload{
|
|
351
|
+
Message: "Automated upload via GO-DUCK GitHub Provider",
|
|
352
|
+
Content: base64.StdEncoding.EncodeToString(data),
|
|
353
|
+
Branch: p.branch,
|
|
354
|
+
}
|
|
355
|
+
bodyBytes, err := json.Marshal(payload)
|
|
356
|
+
if err != nil {
|
|
357
|
+
return "", err
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
req, err := http.NewRequestWithContext(ctx, "PUT", url, bytes.NewBuffer(bodyBytes))
|
|
361
|
+
if err != nil {
|
|
362
|
+
return "", err
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
if p.token != "" {
|
|
366
|
+
req.Header.Set("Authorization", "token "+p.token)
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
client := &http.Client{Timeout: 15 * time.Second}
|
|
370
|
+
resp, err := client.Do(req)
|
|
371
|
+
if err != nil {
|
|
372
|
+
return "", err
|
|
373
|
+
}
|
|
374
|
+
defer resp.Body.Close()
|
|
375
|
+
|
|
376
|
+
if resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusOK {
|
|
377
|
+
return "", fmt.Errorf("GitHub failed to upload %s (Status: %d)", url, resp.StatusCode)
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
return "gh://" + p.repo + "/" + key, nil
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
func (p *GitHubProvider) Download(ctx context.Context, key string) ([]byte, error) {
|
|
384
|
+
if p.owner == "" || p.repo == "" {
|
|
385
|
+
return nil, fmt.Errorf("GitHub provider missing owner or repo in config")
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
url := fmt.Sprintf("https://raw.githubusercontent.com/%s/%s/%s/%s", p.owner, p.repo, p.branch, key)
|
|
389
|
+
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
|
390
|
+
if err != nil {
|
|
391
|
+
return nil, err
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
if p.token != "" {
|
|
395
|
+
req.Header.Set("Authorization", "token "+p.token)
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
client := &http.Client{Timeout: 10 * time.Second}
|
|
399
|
+
resp, err := client.Do(req)
|
|
400
|
+
if err != nil {
|
|
401
|
+
return nil, err
|
|
402
|
+
}
|
|
403
|
+
defer resp.Body.Close()
|
|
404
|
+
|
|
405
|
+
if resp.StatusCode != http.StatusOK {
|
|
406
|
+
return nil, fmt.Errorf("GitHub failed to fetch %s (Status: %d)", url, resp.StatusCode)
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
return io.ReadAll(resp.Body)
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
func (p *GitHubProvider) Delete(ctx context.Context, key string) error {
|
|
413
|
+
return nil
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
func (p *GitHubProvider) GetSignedURL(ctx context.Context, key string, expires time.Duration) (string, error) {
|
|
417
|
+
return "", nil
|
|
418
|
+
}
|
|
419
|
+
`;
|
|
420
|
+
|
|
421
|
+
const storageControllerGo = `package controllers
|
|
422
|
+
|
|
423
|
+
import (
|
|
424
|
+
"fmt"
|
|
425
|
+
"io"
|
|
426
|
+
"net/http"
|
|
427
|
+
"strings"
|
|
428
|
+
"time"
|
|
429
|
+
|
|
430
|
+
"github.com/gin-gonic/gin"
|
|
431
|
+
"{{app_name}}/internal/storage"
|
|
432
|
+
)
|
|
433
|
+
|
|
434
|
+
type StorageController struct{}
|
|
435
|
+
|
|
436
|
+
func (ctrl *StorageController) Upload(c *gin.Context) {
|
|
437
|
+
if len(storage.Providers) == 0 {
|
|
438
|
+
c.JSON(http.StatusNotImplemented, gin.H{"error": "No storage providers enabled"})
|
|
439
|
+
return
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
providerName := c.Query("provider")
|
|
443
|
+
if providerName == "" {
|
|
444
|
+
for k := range storage.Providers {
|
|
445
|
+
providerName = k
|
|
446
|
+
break
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
provider, exists := storage.Providers[providerName]
|
|
451
|
+
if !exists {
|
|
452
|
+
c.JSON(http.StatusBadRequest, gin.H{"error": "Requested storage provider is not enabled: " + providerName})
|
|
453
|
+
return
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
file, header, err := c.Request.FormFile("file")
|
|
457
|
+
if err != nil {
|
|
458
|
+
c.JSON(http.StatusBadRequest, gin.H{"error": "No file uploaded (form-data: 'file')"})
|
|
459
|
+
return
|
|
460
|
+
}
|
|
461
|
+
defer file.Close()
|
|
462
|
+
|
|
463
|
+
// Optional precise structured folder path mapping
|
|
464
|
+
folder := c.PostForm("folder")
|
|
465
|
+
|
|
466
|
+
data, err := io.ReadAll(file)
|
|
467
|
+
if err != nil {
|
|
468
|
+
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to read file"})
|
|
469
|
+
return
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
timestamp := time.Now().UnixNano()
|
|
473
|
+
var key string
|
|
474
|
+
if folder != "" {
|
|
475
|
+
folder = strings.Trim(folder, "/")
|
|
476
|
+
key = fmt.Sprintf("%s/%d_%s", folder, timestamp, header.Filename)
|
|
477
|
+
} else {
|
|
478
|
+
key = fmt.Sprintf("%d_%s", timestamp, header.Filename)
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
_, err = provider.Upload(c.Request.Context(), key, data)
|
|
482
|
+
if err != nil {
|
|
483
|
+
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
484
|
+
return
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
c.JSON(http.StatusOK, gin.H{
|
|
488
|
+
"message": "Upload successful",
|
|
489
|
+
"key": key,
|
|
490
|
+
"provider": providerName,
|
|
491
|
+
"url": fmt.Sprintf("/api/storage/download/%s?provider=%s", key, providerName),
|
|
492
|
+
})
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
func (ctrl *StorageController) Download(c *gin.Context) {
|
|
496
|
+
if len(storage.Providers) == 0 {
|
|
497
|
+
c.JSON(http.StatusNotImplemented, gin.H{"error": "No storage providers enabled"})
|
|
498
|
+
return
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
key := c.Param("key")
|
|
502
|
+
key = strings.TrimPrefix(key, "/")
|
|
503
|
+
|
|
504
|
+
prefixes := []string{"sftp:/", "s3:/", "gs:/", "gh:/"}
|
|
505
|
+
for _, p := range prefixes {
|
|
506
|
+
if strings.HasPrefix(key, p) {
|
|
507
|
+
parts := strings.Split(key, "/")
|
|
508
|
+
if len(parts) > 0 {
|
|
509
|
+
key = parts[len(parts)-1]
|
|
510
|
+
}
|
|
511
|
+
break
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
providerName := c.Query("provider")
|
|
516
|
+
if providerName == "" {
|
|
517
|
+
for k := range storage.Providers {
|
|
518
|
+
providerName = k
|
|
519
|
+
break
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
provider, exists := storage.Providers[providerName]
|
|
524
|
+
if !exists {
|
|
525
|
+
c.JSON(http.StatusBadRequest, gin.H{"error": "Requested storage provider is not enabled: " + providerName})
|
|
526
|
+
return
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
data, err := provider.Download(c.Request.Context(), key)
|
|
530
|
+
if err != nil {
|
|
531
|
+
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
|
532
|
+
return
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
contentType := http.DetectContentType(data)
|
|
536
|
+
c.Data(http.StatusOK, contentType, data)
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
func (ctrl *StorageController) CrossScanDownload(c *gin.Context) {
|
|
540
|
+
if len(storage.Providers) == 0 {
|
|
541
|
+
c.JSON(http.StatusNotImplemented, gin.H{"error": "No storage providers enabled"})
|
|
542
|
+
return
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
key := c.Param("key")
|
|
546
|
+
key = strings.TrimPrefix(key, "/")
|
|
547
|
+
|
|
548
|
+
prefixes := []string{"sftp:/", "s3:/", "gs:/", "gh:/"}
|
|
549
|
+
for _, p := range prefixes {
|
|
550
|
+
if strings.HasPrefix(key, p) {
|
|
551
|
+
parts := strings.Split(key, "/")
|
|
552
|
+
if len(parts) > 0 {
|
|
553
|
+
key = parts[len(parts)-1]
|
|
554
|
+
}
|
|
555
|
+
break
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// Distributed Cross-Scan: Iterate over every single active data lake node until the file is found
|
|
560
|
+
for pName, provider := range storage.Providers {
|
|
561
|
+
data, err := provider.Download(c.Request.Context(), key)
|
|
562
|
+
if err == nil && len(data) > 0 {
|
|
563
|
+
// File successfully located in this provider!
|
|
564
|
+
contentType := http.DetectContentType(data)
|
|
565
|
+
|
|
566
|
+
// Expose which node successfully returned the payload
|
|
567
|
+
c.Header("X-Storage-Provider-Found", pName)
|
|
568
|
+
c.Data(http.StatusOK, contentType, data)
|
|
569
|
+
return
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
c.JSON(http.StatusNotFound, gin.H{"error": "Cross-Scan Failed: File not found across any active storage providers"})
|
|
574
|
+
}
|
|
575
|
+
`
|
|
576
|
+
|
|
577
|
+
const appName = config.name;
|
|
578
|
+
await fs.writeFile(path.join(storageDir, 'storage.go'), storageInterfaceGo);
|
|
579
|
+
await fs.writeFile(path.join(storageDir, 'bootstrap.go'), bootstrapperGo.replace(/{{app_name}}/g, appName));
|
|
580
|
+
await fs.writeFile(path.join(storageDir, 's3.go'), s3ProviderGo.replace(/{{app_name}}/g, appName));
|
|
581
|
+
await fs.writeFile(path.join(storageDir, 'gcs.go'), gcsProviderGo.replace(/{{app_name}}/g, appName));
|
|
582
|
+
await fs.writeFile(path.join(storageDir, 'provider.go'), providerFactoryGo.replace(/{{app_name}}/g, appName));
|
|
583
|
+
|
|
584
|
+
const controllersDir = path.join(outputDir, 'controllers');
|
|
585
|
+
await fs.ensureDir(controllersDir);
|
|
586
|
+
await fs.writeFile(path.join(controllersDir, 'storage_controller.go'), storageControllerGo.replace(/{{app_name}}/g, appName));
|
|
587
|
+
|
|
588
|
+
console.log(chalk.gray(' Generated Native Object Storage Engine (Includes HTTP StorageController endpoints)'));
|
|
589
|
+
};
|