Skip to content

Commit d7f4d18

Browse files
authored
Merge branch 'main' into copilot/update-dependencies-for-security
2 parents e37f32f + fba4890 commit d7f4d18

9 files changed

Lines changed: 720 additions & 350 deletions

File tree

go.mod

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
module github.com/tursodatabase/turso-cli
22

3-
go 1.24.0
3+
go 1.25
44

5-
toolchain go1.24.11
5+
toolchain go1.25.0
66

77
require (
88
github.com/Clever/csvlint v0.3.0

internal/cmd/db_create.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ func CreateDatabase(name string) error {
113113
return err
114114
}
115115

116-
seed, err := parseDBSeedFlags(client, isAWS, remoteEncryptionCipherFlag, multipartFlag)
116+
seed, err := parseDBSeedFlags(client, isAWS, remoteEncryptionCipherFlag)
117117
if err != nil {
118118
return err
119119
}

internal/cmd/db_import.go

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,11 @@ import (
88
"github.com/spf13/cobra"
99
)
1010

11-
var multipartFlag bool
12-
1311
func init() {
1412
dbCmd.AddCommand(importCmd)
1513
addGroupFlag(importCmd)
1614
addRemoteEncryptionKeyFlag(importCmd)
1715
addRemoteEncryptionCipherFlag(importCmd)
18-
importCmd.Flags().BoolVar(&multipartFlag, "multipart", false, "force multipart upload")
1916
}
2017

2118
var importCmd = &cobra.Command{

internal/cmd/group_flag.go

Lines changed: 36 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ func parseTimestampFlag() (*time.Time, error) {
6565
return &timestamp, nil
6666
}
6767

68-
func parseDBSeedFlags(client *turso.Client, isAWS bool, cipher string, multipart bool) (*turso.DBSeed, error) {
68+
func parseDBSeedFlags(client *turso.Client, isAWS bool, cipher string) (*turso.DBSeed, error) {
6969
if countFlags(fromDBFlag, fromDumpFlag, fromFileFlag, fromDumpURLFlag, fromCSVFlag) > 1 {
7070
return nil, errors.New("only one of --from prefixed flags can be used at a time")
7171
}
@@ -86,7 +86,7 @@ func parseDBSeedFlags(client *turso.Client, isAWS bool, cipher string, multipart
8686
}
8787

8888
if fromFileFlag != "" {
89-
return handleDBFile(client, fromFileFlag, isAWS, cipher, multipart)
89+
return handleDBFile(client, fromFileFlag, isAWS, cipher)
9090
}
9191

9292
if fromDumpFlag != "" {
@@ -98,7 +98,7 @@ func parseDBSeedFlags(client *turso.Client, isAWS bool, cipher string, multipart
9898
if err != nil {
9999
return nil, err
100100
}
101-
return handleCSVFile(client, fromCSVFlag, csvTableNameFlag, csvSeparator, cipher, multipart)
101+
return handleCSVFile(client, fromCSVFlag, csvTableNameFlag, csvSeparator, cipher)
102102
}
103103
if fromDumpURLFlag != "" {
104104
return handleDumpURL(fromDumpURLFlag)
@@ -191,6 +191,20 @@ func countFlags(flags ...string) (count int) {
191191
}
192192

193193
const MaxAWSDBSizeBytes = 1024 * 1024 * 1024 * 20 // 20 GB
194+
195+
func humanReadableSize(bytes int64) string {
196+
const unit = 1024
197+
if bytes < unit {
198+
return fmt.Sprintf("%d B", bytes)
199+
}
200+
div, exp := int64(unit), 0
201+
for n := bytes / unit; n >= unit; n /= unit {
202+
div *= unit
203+
exp++
204+
}
205+
return fmt.Sprintf("%.1f %cB", float64(bytes)/float64(div), "KMGTPE"[exp])
206+
}
207+
194208
func checkIfDump(filename string) (bool, error) {
195209
file, err := os.Open(filename)
196210
if err != nil {
@@ -307,9 +321,11 @@ func sqliteFileIntegrityChecks(file string, cipher string) error {
307321
if flags.Debug() {
308322
log.Printf("Running integrity check...")
309323
}
310-
_, err = exec.Command("sqlite3", file, "pragma quick_check;").CombinedOutput()
324+
spinner := prompt.Spinner(fmt.Sprintf("Validating database file (%s)...", humanReadableSize(fileInfo.Size())))
325+
err = runQuickCheck(file)
326+
spinner.Stop()
311327
if err != nil {
312-
return fmt.Errorf("integrity check on database failed: %w", err)
328+
return err
313329
}
314330

315331
// validate reserved bytes if encryption cipher is specified
@@ -323,21 +339,28 @@ func sqliteFileIntegrityChecks(file string, cipher string) error {
323339
return nil
324340
}
325341

326-
func handleDBFileAWS(file string, cipher string, multipart bool) (*turso.DBSeed, error) {
342+
func runQuickCheck(file string) error {
343+
cmd := exec.Command("sqlite3", file, "pragma quick_check;")
344+
if err := cmd.Run(); err != nil {
345+
return fmt.Errorf("integrity check failed: %w", err)
346+
}
347+
return nil
348+
}
349+
350+
func handleDBFileAWS(file string, cipher string) (*turso.DBSeed, error) {
327351
if err := sqliteFileIntegrityChecks(file, cipher); err != nil {
328352
return nil, err
329353
}
330354

331355
seed := &turso.DBSeed{
332-
Type: "database_upload",
333-
Filepath: file,
334-
Multipart: multipart,
356+
Type: "database_upload",
357+
Filepath: file,
335358
}
336359

337360
return seed, nil
338361
}
339362

340-
func handleDBFile(client *turso.Client, file string, isAWS bool, cipher string, multipart bool) (*turso.DBSeed, error) {
363+
func handleDBFile(client *turso.Client, file string, isAWS bool, cipher string) (*turso.DBSeed, error) {
341364
if err := checkFileExists(file); err != nil {
342365
return nil, err
343366
}
@@ -346,7 +369,7 @@ func handleDBFile(client *turso.Client, file string, isAWS bool, cipher string,
346369
}
347370

348371
if isAWS {
349-
return handleDBFileAWS(file, cipher, multipart)
372+
return handleDBFileAWS(file, cipher)
350373
}
351374

352375
if err := checkSQLiteFile(file); err != nil {
@@ -416,7 +439,7 @@ func dumpSQLiteDatabase(database string, dump *os.File) error {
416439
return nil
417440
}
418441

419-
func handleCSVFile(client *turso.Client, file, csvTableName string, separator rune, cipher string, multipart bool) (*turso.DBSeed, error) {
442+
func handleCSVFile(client *turso.Client, file, csvTableName string, separator rune, cipher string) (*turso.DBSeed, error) {
420443
if err := checkFileExists(file); err != nil {
421444
return nil, err
422445
}
@@ -446,7 +469,7 @@ func handleCSVFile(client *turso.Client, file, csvTableName string, separator ru
446469
return nil, err
447470
}
448471

449-
seed, err := handleDBFile(client, tempDB.Name(), false, cipher, multipart)
472+
seed, err := handleDBFile(client, tempDB.Name(), false, cipher)
450473
if err != nil {
451474
return nil, err
452475
}

internal/cmd/group_flag_test.go

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
package cmd
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"os/exec"
7+
"path/filepath"
8+
"testing"
9+
10+
"github.com/stretchr/testify/require"
11+
)
12+
13+
// createTestDatabase creates a valid WAL-mode test database with approximately the given size
14+
func createTestDatabase(t *testing.T, sizeBytes int) string {
15+
t.Helper()
16+
17+
if _, err := exec.LookPath("sqlite3"); err != nil {
18+
t.Skip("sqlite3 not available, skipping test")
19+
}
20+
21+
tmpDir := t.TempDir()
22+
dbPath := filepath.Join(tmpDir, "test.db")
23+
24+
// Create database with correct settings for Turso
25+
cmd := exec.Command("sqlite3", dbPath,
26+
"PRAGMA page_size=4096;",
27+
"PRAGMA journal_mode=WAL;",
28+
"CREATE TABLE data (id INTEGER PRIMARY KEY, blob BLOB);")
29+
require.NoError(t, cmd.Run(), "failed to create test database")
30+
31+
// Fill with data to reach target size
32+
if sizeBytes > 0 {
33+
rowSize := 1000 // ~1KB per row
34+
numRows := sizeBytes / rowSize
35+
if numRows < 1 {
36+
numRows = 1
37+
}
38+
for i := 0; i < numRows; i++ {
39+
cmd = exec.Command("sqlite3", dbPath,
40+
fmt.Sprintf("INSERT INTO data (blob) VALUES (randomblob(%d));", rowSize))
41+
require.NoError(t, cmd.Run())
42+
}
43+
}
44+
45+
return dbPath
46+
}
47+
48+
func TestRunQuickCheck(t *testing.T) {
49+
if _, err := exec.LookPath("sqlite3"); err != nil {
50+
t.Skip("sqlite3 not available, skipping test")
51+
}
52+
53+
t.Run("valid database succeeds", func(t *testing.T) {
54+
dbPath := createTestDatabase(t, 10*1024) // 10KB
55+
err := runQuickCheck(dbPath)
56+
require.NoError(t, err)
57+
})
58+
59+
t.Run("corrupted database returns error", func(t *testing.T) {
60+
tmpDir := t.TempDir()
61+
dbPath := filepath.Join(tmpDir, "corrupt.db")
62+
63+
// Create a file with garbage data
64+
err := os.WriteFile(dbPath, []byte("not a valid sqlite database content here"), 0644)
65+
require.NoError(t, err)
66+
67+
err = runQuickCheck(dbPath)
68+
require.Error(t, err)
69+
require.Contains(t, err.Error(), "integrity check failed")
70+
})
71+
72+
t.Run("nonexistent file returns error", func(t *testing.T) {
73+
err := runQuickCheck("/nonexistent/path/db.sqlite")
74+
require.Error(t, err)
75+
})
76+
}

internal/turso/databases.go

Lines changed: 12 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,6 @@ import (
1515
"github.com/tursodatabase/turso-cli/internal/prompt"
1616
)
1717

18-
const multipartUploadThresholdBytes = 100 * 1024 * 1024 // 100MB
19-
2018
type Database struct {
2119
ID string `json:"dbId" mapstructure:"dbId"`
2220
Name string
@@ -133,8 +131,7 @@ type DBSeed struct {
133131
Timestamp *time.Time `json:"timestamp,omitempty"`
134132
// This is only used locally when uploading a database file and
135133
// never passed to the control plane as JSON.
136-
Filepath string `json:"-"`
137-
Multipart bool `json:"-"`
134+
Filepath string `json:"-"`
138135
}
139136

140137
type RemoteEncryption struct {
@@ -158,17 +155,14 @@ type CreateDatabaseBody struct {
158155
func (d *DatabasesClient) Create(name, location, image, extensions, group string, schema string, isSchema bool, seed *DBSeed, sizeLimit, remoteEncryptionCipher, remoteEncryptionKey string, spinner *prompt.SpinnerT) (*CreateDatabaseResponse, error) {
159156
isTursoServerUpload := seed != nil && seed.Type == "database_upload" && seed.Filepath != ""
160157
var uploadFilepath string
161-
var useMultipart bool
162158
var params CreateDatabaseBody
163159
if isTursoServerUpload {
164160
uploadFilepath = seed.Filepath
165-
useMultipart = seed.Multipart
166161
// Clear the unused seed parameters, only Type=database_upload is used.
167162
seed.Filepath = ""
168163
seed.Name = ""
169164
seed.URL = ""
170165
seed.Timestamp = nil
171-
seed.Multipart = false
172166
params = CreateDatabaseBody{
173167
Name: name,
174168
Location: location,
@@ -216,7 +210,7 @@ func (d *DatabasesClient) Create(name, location, image, extensions, group string
216210
}
217211

218212
if isTursoServerUpload {
219-
if _, err = d.UploadDatabaseAWS(data, group, uploadFilepath, remoteEncryptionCipher, remoteEncryptionKey, useMultipart, spinner); err != nil {
213+
if _, err = d.UploadDatabaseAWS(data, group, uploadFilepath, remoteEncryptionCipher, remoteEncryptionKey, spinner); err != nil {
220214
// Clean up the database if the upload fails
221215
if deleteErr := d.Delete(data.Database.Name); deleteErr != nil {
222216
fmt.Printf("%v", deleteErr)
@@ -237,36 +231,26 @@ func (d *DatabasesClient) Create(name, location, image, extensions, group string
237231
// This call happens in DatabasesClient.Create() above, after which it calls this function.
238232
// 2. This function creates a DB token for the newly-created DB, and then calls turso-server to upload the database file.
239233
// turso-server will perform validations on the file and 'activate' the db if everything is ok.
240-
func (d *DatabasesClient) UploadDatabaseAWS(resp *CreateDatabaseResponse, group, uploadFilepath, remoteEncryptionCipher, remoteEncryptionKey string, useMultipart bool, spinner *prompt.SpinnerT) (*CreateDatabaseResponse, error) {
241-
// Create a short-lived DB token for the newly created database to facilitate the upload
242-
token, err := d.Token(resp.Database.Name, "1h", false, nil, nil)
243-
if err != nil {
244-
return nil, fmt.Errorf("could not create database token: %w", err)
234+
func (d *DatabasesClient) UploadDatabaseAWS(resp *CreateDatabaseResponse, group, uploadFilepath, remoteEncryptionCipher, remoteEncryptionKey string, spinner *prompt.SpinnerT) (*CreateDatabaseResponse, error) {
235+
dbName := resp.Database.Name
236+
tokenTTL := 5 * time.Minute
237+
tokenProvider := func() (string, error) {
238+
return d.Token(dbName, "5m", false, nil, nil)
245239
}
246240

247241
baseURL, err := url.Parse(fmt.Sprintf("https://%s", resp.Database.Hostname))
248242
if err != nil {
249243
return nil, fmt.Errorf("unable to create TursoServerClient: %v", err)
250244
}
251-
tursoServerClient, err := NewTursoServerClient(baseURL, token, d.client.cliVersion, d.client.Org)
245+
tursoServerClient, err := NewTursoServerClient(baseURL, tokenProvider, tokenTTL, d.client.cliVersion, d.client.Org)
252246
if err != nil {
253247
return nil, fmt.Errorf("could not create Turso server client: %w", err)
254248
}
255249

256250
// Upload the database file
257251
spinner.Text(fmt.Sprintf("Uploading database %s in group %s, this may take a while...", internal.Emph(resp.Database.Name), internal.Emph(group)))
258252

259-
stat, err := os.Stat(uploadFilepath)
260-
if err != nil {
261-
return nil, fmt.Errorf("failed to fetch file size %s: %w", uploadFilepath, err)
262-
}
263-
264-
uploadFunc := tursoServerClient.UploadFileSinglePart
265-
if useMultipart || stat.Size() > multipartUploadThresholdBytes {
266-
uploadFunc = tursoServerClient.UploadFileMultipart
267-
}
268-
269-
err = uploadFunc(uploadFilepath, remoteEncryptionCipher, remoteEncryptionKey, func(progressPct int, uploadedBytes int64, totalBytes int64, elapsedTime time.Duration, done bool) {
253+
err = tursoServerClient.UploadFileMultipart(uploadFilepath, remoteEncryptionCipher, remoteEncryptionKey, func(progressPct int, uploadedBytes int64, totalBytes int64, elapsedTime time.Duration, done bool) {
270254
totalSeconds := int(elapsedTime.Seconds())
271255
minutes := totalSeconds / 60
272256
seconds := totalSeconds % 60
@@ -304,15 +288,14 @@ func (d *DatabasesClient) Export(dbName, dbUrl, outputFile string, withMetadata
304288
return fmt.Errorf("file %s already exists, use `--overwrite` flag to overwrite it", outputFile)
305289
}
306290
}
307-
token, err := d.Token(dbName, "1h", false, nil, nil)
308-
if err != nil {
309-
return fmt.Errorf("could not create database token: %w", err)
291+
tokenProvider := func() (string, error) {
292+
return d.Token(dbName, "1h", false, nil, nil)
310293
}
311294
baseURL, err := url.Parse(dbUrl)
312295
if err != nil {
313296
return fmt.Errorf("could not parse database URL: %w", err)
314297
}
315-
tursoServerClient, err := NewTursoServerClient(baseURL, token, d.client.cliVersion, d.client.Org)
298+
tursoServerClient, err := NewTursoServerClient(baseURL, tokenProvider, time.Hour, d.client.cliVersion, d.client.Org)
316299
if err != nil {
317300
return fmt.Errorf("could not create Turso server client: %w", err)
318301
}

internal/turso/turso.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,10 @@ func New(base *url.URL, token string, cliVersion string, org string) *Client {
6363
return c
6464
}
6565

66+
func (t *Client) SetToken(token string) {
67+
t.token = token
68+
}
69+
6670
func (t *Client) newRequest(method, urlPath string, body io.Reader, extraHeaders map[string]string) (*http.Request, error) {
6771
if _, exists := extraHeaders["Content-Type"]; !exists {
6872
return nil, errors.New("content type is required")

0 commit comments

Comments
 (0)