Files
s3/weed/filer/mysql/mysql_store.go
zooqueen dd74d3e6d6 Replace AGPL MinIO fork with Apache-2.0 SeaweedFS fork; rebrand CLI weed->s3
hanzoai/s3 was an AGPL-3.0 MinIO fork whose network-copyleft blocks embedding
and reselling as a managed service. Replace it wholesale with a fork of
SeaweedFS (github.com/seaweedfs/seaweedfs, Apache-2.0, Go) which ships a
built-in S3 gateway + Filer.

- License: AGPL-3.0 -> Apache-2.0 (upstream LICENSE kept verbatim, incl.
  "Copyright 2025 Chris Lu"); NOTICE added crediting SeaweedFS. No AGPL/BUSL/
  SSPL/GPL or enterprise ("ee") files remain.
- Module: go.mod path set to github.com/hanzoai/s3 with a self-replace
  (replace github.com/seaweedfs/seaweedfs => ./) so the ~1800 internal Go files
  keep their upstream import paths with zero churn. Internal identifiers and
  import paths are intentionally NOT renamed.
- CLI rebrand: the binary is built as `s3` (was `weed`); root usage, per-command
  help/usage/flag strings, shell autocomplete program name, and the product name
  in CLI output now read "Hanzo S3" / `s3`. Users never see `weed`.
  (e.g. `s3 server`, `s3 filer`, `s3 master`, `s3 volume`, `s3 s3`, `s3 mount`.)
- Build: `make` (or `go build -o s3 ./weed`) builds the `s3` binary green;
  `cmd/weed-db` and `cmd/weed-sql` also build; `go vet ./weed/command/` clean.
- README/LLM.md rewritten for Hanzo S3; AGENTS.md/CLAUDE.md symlinks preserved.
- Dropped a 33MB prebuilt Mach-O `stress` binary upstream had committed; `.gitignore`
  updated to ignore the `s3` build output.
2026-06-18 23:41:25 -07:00

170 lines
5.3 KiB
Go

package mysql
import (
"crypto/tls"
"crypto/x509"
"database/sql"
"errors"
"fmt"
"os"
"strings"
"time"
"github.com/go-sql-driver/mysql"
"github.com/seaweedfs/seaweedfs/weed/filer"
"github.com/seaweedfs/seaweedfs/weed/filer/abstract_sql"
"github.com/seaweedfs/seaweedfs/weed/util"
)
const (
CONNECTION_URL_PATTERN = "%s:%s@tcp(%s:%d)/%s?collation=utf8mb4_bin"
)
func init() {
filer.Stores = append(filer.Stores, &MysqlStore{})
}
type MysqlStore struct {
abstract_sql.AbstractSqlStore
}
func (store *MysqlStore) GetName() string {
return "mysql"
}
func (store *MysqlStore) Initialize(configuration util.Configuration, prefix string) (err error) {
// Absent key keeps a pooled default; an explicit 0 disables the idle pool.
configuration.SetDefault(prefix+"connection_max_idle", 2)
// Default on so minimal configs avoid the duplicate-key roundtrip the
// inode-index KvPut would otherwise emit on every write.
configuration.SetDefault(prefix+"enableUpsert", true)
return store.initialize(
configuration.GetString(prefix+"dsn"),
configuration.GetString(prefix+"upsertQuery"),
configuration.GetBool(prefix+"enableUpsert"),
configuration.GetString(prefix+"username"),
configuration.GetString(prefix+"password"),
configuration.GetString(prefix+"hostname"),
configuration.GetInt(prefix+"port"),
configuration.GetString(prefix+"database"),
configuration.GetInt(prefix+"connection_max_idle"),
configuration.GetInt(prefix+"connection_max_open"),
configuration.GetInt(prefix+"connection_max_lifetime_seconds"),
configuration.GetBool(prefix+"interpolateParams"),
configuration.GetBool(prefix+"enable_tls"),
configuration.GetString(prefix+"ca_crt"),
configuration.GetString(prefix+"client_crt"),
configuration.GetString(prefix+"client_key"),
configuration.GetBool(prefix+"tls_insecure_skip_verify"),
configuration.GetString(prefix+"tls_server_name"),
)
}
func (store *MysqlStore) initialize(dsn string, upsertQuery string, enableUpsert bool, user, password, hostname string, port int, database string, maxIdle, maxOpen,
maxLifetimeSeconds int, interpolateParams bool, enableTls bool, caCrtDir string, clientCrtDir string, clientKeyDir string,
tlsInsecureSkipVerify bool, tlsServerName string) (err error) {
store.SupportBucketTable = false
if !enableUpsert {
upsertQuery = ""
} else if upsertQuery == "" {
upsertQuery = DefaultUpsertQuery
}
gen := &SqlGenMysql{
CreateTableSqlTemplate: "",
DropTableSqlTemplate: "DROP TABLE `%s`",
UpsertQueryTemplate: upsertQuery,
}
store.SqlGenerator = gen
store.RetryableErrorCallback = func(err error) bool {
var mysqlError *mysql.MySQLError
if errors.As(err, &mysqlError) {
if mysqlError.Number == 1213 { // ER_LOCK_DEADLOCK
return true
}
if mysqlError.Number == 1205 { // ER_LOCK_WAIT_TIMEOUT
return true
}
}
return false
}
if dsn == "" {
dsn = fmt.Sprintf(CONNECTION_URL_PATTERN, user, password, hostname, port, database)
if interpolateParams {
dsn += "&interpolateParams=true"
}
}
cfg, err := mysql.ParseDSN(dsn)
if err != nil {
return fmt.Errorf("can not parse DSN error:%w", err)
}
if enableTls {
tlsConfig := &tls.Config{
MinVersion: tls.VersionTLS12,
InsecureSkipVerify: tlsInsecureSkipVerify,
ServerName: tlsServerName,
}
// When ca_crt is empty, leave RootCAs nil so Go falls back to the
// system trust store. This is the common case for managed databases
// (RDS, Aiven, ...) whose certs chain to a public CA already on the host.
if caCrtDir != "" {
rootCertPool := x509.NewCertPool()
pem, err := os.ReadFile(caCrtDir)
if err != nil {
return fmt.Errorf("read ca_crt %s: %w", caCrtDir, err)
}
if ok := rootCertPool.AppendCertsFromPEM(pem); !ok {
return fmt.Errorf("failed to append root certificate from %s", caCrtDir)
}
tlsConfig.RootCAs = rootCertPool
}
// Only attempt to load a client keypair when at least one of the paths is
// set. If either is set, both must load successfully — silently skipping
// a typo'd path used to mask broken mTLS setups as confusing handshake
// failures.
if clientCrtDir != "" || clientKeyDir != "" {
cert, err := tls.LoadX509KeyPair(clientCrtDir, clientKeyDir)
if err != nil {
return fmt.Errorf("load mysql client keypair (crt=%s key=%s): %w", clientCrtDir, clientKeyDir, err)
}
tlsConfig.Certificates = []tls.Certificate{cert}
}
// Set TLS directly on the parsed Config rather than registering a global
// "mysql-tls" entry — the global registry is process-wide and would be
// overwritten if a second MysqlStore is initialized with different TLS
// settings.
cfg.TLS = tlsConfig
}
connector, err := mysql.NewConnector(cfg)
if err != nil {
return fmt.Errorf("can not create mysql connector for %s error:%w", maskedDSN(cfg), err)
}
store.DB = sql.OpenDB(connector)
store.DB.SetMaxIdleConns(maxIdle)
store.DB.SetMaxOpenConns(maxOpen)
store.DB.SetConnMaxLifetime(time.Duration(maxLifetimeSeconds) * time.Second)
if err = store.DB.Ping(); err != nil {
return fmt.Errorf("connect to %s error:%v", maskedDSN(cfg), err)
}
ConfigureListOrdering(store.DB, gen)
return nil
}
func maskedDSN(cfg *mysql.Config) string {
if cfg.Passwd == "" {
return cfg.FormatDSN()
}
return strings.ReplaceAll(cfg.FormatDSN(), cfg.Passwd, "<ADAPTED>")
}