mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-13 04:14:56 +00:00
fix(migrate): RFC 8089-compliant file:// URL for Windows paths
golang-migrate's file source driver parsed "file://F:\\project\\..." with "F" as host and ":\\..." as port → "invalid port" error, so upgrade/migrate commands crashed on Windows before applying any SQL. Format the source URL per RFC 8089: forward-slash separators and a leading "/" before the drive letter → "file:///F:/project/...". POSIX paths pass through unchanged (leading "/" already present). Shape-driven, not OS-driven: no runtime.GOOS branch needed, which also lets the unit test cover both shapes on a single runner. Closes #872
This commit is contained in:
1 parent
4988f57a9c
commit
935b58b7a3
2 files changed
+71
-1
No files matched your search
+29
-1
@@ -9,6 +9,7 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/golang-migrate/migrate/v4"
|
||||
_ "github.com/golang-migrate/migrate/v4/database/postgres"
|
||||
@@ -38,9 +39,36 @@ func resolveMigrationsDir() string {
|
||||
return filepath.Join(filepath.Dir(exe), "migrations")
|
||||
}
|
||||
|
||||
// absoluteToFileURI formats an already-absolute path into an RFC 8089-compliant
|
||||
// file:// URL. golang-migrate's file source driver rejects Windows paths like
|
||||
// "file://F:\\project\\migrations" because "F" is parsed as the host and
|
||||
// ":\\..." as the port. The fix is shape-driven (presence of a drive-letter
|
||||
// colon at index 1), so no runtime.GOOS branch is needed — the same code is
|
||||
// correct for POSIX inputs ("/app/x" → "file:///app/x") and for Windows inputs
|
||||
// on any OS ("F:\\x" → "file:///F:/x"). strings.ReplaceAll covers the case
|
||||
// where a Windows path is seen on a non-Windows runner (filepath.ToSlash is a
|
||||
// no-op outside Windows).
|
||||
func absoluteToFileURI(abs string) string {
|
||||
abs = strings.ReplaceAll(filepath.ToSlash(abs), `\`, `/`)
|
||||
if len(abs) >= 2 && abs[1] == ':' {
|
||||
abs = "/" + abs
|
||||
}
|
||||
return "file://" + abs
|
||||
}
|
||||
|
||||
// migrationsSourceURL resolves dir to an absolute path and formats it for
|
||||
// golang-migrate's file source driver.
|
||||
func migrationsSourceURL(dir string) string {
|
||||
abs, err := filepath.Abs(dir)
|
||||
if err != nil {
|
||||
abs = dir
|
||||
}
|
||||
return absoluteToFileURI(abs)
|
||||
}
|
||||
|
||||
func newMigrator(dsn string) (*migrate.Migrate, error) {
|
||||
dir := resolveMigrationsDir()
|
||||
m, err := migrate.New("file://"+dir, dsn)
|
||||
m, err := migrate.New(migrationsSourceURL(dir), dsn)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create migrator: %w", err)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestAbsoluteToFileURI(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
in string
|
||||
want string
|
||||
}{
|
||||
{"posix absolute", "/app/migrations", "file:///app/migrations"},
|
||||
// Windows drive-letter path with backslashes: the exact shape
|
||||
// golang-migrate needs. Before the fix, "file://F:\\..." was parsed
|
||||
// with "F" as host and ":\\..." as port → "invalid port" error.
|
||||
{"windows backslash", `F:\project\goclaw\migrations`, "file:///F:/project/goclaw/migrations"},
|
||||
{"windows mixed separators", `C:/already/forward`, "file:///C:/already/forward"},
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
if got := absoluteToFileURI(c.in); got != c.want {
|
||||
t.Errorf("got %q, want %q", got, c.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestMigrationsSourceURLRelative verifies the helper resolves a relative
|
||||
// input via filepath.Abs before formatting — exact output depends on CWD,
|
||||
// so we only assert invariants that must hold on every runner.
|
||||
func TestMigrationsSourceURLRelative(t *testing.T) {
|
||||
got := migrationsSourceURL("migrations")
|
||||
if !strings.HasPrefix(got, "file://") {
|
||||
t.Fatalf("missing file:// prefix: %q", got)
|
||||
}
|
||||
if !strings.Contains(filepath.ToSlash(got), "/migrations") {
|
||||
t.Errorf("missing /migrations segment: %q", got)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user