Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
df1d5cb8ff |
@@ -269,9 +269,10 @@ All three subcommands operate on a single SQLite database file:
|
|||||||
directory.
|
directory.
|
||||||
- Each subcommand first prints one line on stderr naming the database it uses,
|
- Each subcommand first prints one line on stderr naming the database it uses,
|
||||||
before anything else it prints (progress displays included), whether or not
|
before anything else it prints (progress displays included), whether or not
|
||||||
stderr is a terminal. The line gives the path made absolute against the
|
stderr is a terminal. The line gives the path with the working directory put
|
||||||
working directory, escaped like the paths in a report (see "Report output
|
in front of it when it is relative, and otherwise as set (a `..` in it stays,
|
||||||
format"), and says where the path came from:
|
so the line names the same file), escaped like the paths in a report (see
|
||||||
|
"Report output format"), and says where the path came from:
|
||||||
|
|
||||||
```
|
```
|
||||||
scan: new database /var/lib/sfdupes/db.sqlite (default)
|
scan: new database /var/lib/sfdupes/db.sqlite (default)
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"slices"
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"golang.org/x/sys/unix"
|
"golang.org/x/sys/unix"
|
||||||
@@ -92,21 +93,30 @@ var errScanRunning = errors.New("another scan is running")
|
|||||||
|
|
||||||
// databasePath resolves the database location: SFDUPES_DATABASE when
|
// databasePath resolves the database location: SFDUPES_DATABASE when
|
||||||
// set and non-empty, the compiled-in default otherwise. It returns the
|
// set and non-empty, the compiled-in default otherwise. It returns the
|
||||||
// path made absolute against the working directory, and where the path
|
// path, with the working directory put in front of it when it is
|
||||||
// came from, "default" or "from SFDUPES_DATABASE", for the line each
|
// relative, and where the path came from, "default" or "from
|
||||||
// subcommand prints first on stderr.
|
// SFDUPES_DATABASE", for the line each subcommand prints first on
|
||||||
|
// stderr.
|
||||||
func databasePath() (string, string, error) {
|
func databasePath() (string, string, error) {
|
||||||
path, source := defaultDatabasePath, "default"
|
path, source := defaultDatabasePath, "default"
|
||||||
if p := os.Getenv(databaseEnv); p != "" {
|
if p := os.Getenv(databaseEnv); p != "" {
|
||||||
path, source = p, "from "+databaseEnv
|
path, source = p, "from "+databaseEnv
|
||||||
}
|
}
|
||||||
|
|
||||||
abs, err := filepath.Abs(path)
|
if filepath.IsAbs(path) {
|
||||||
|
return path, source, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
wd, err := os.Getwd()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", fmt.Errorf("resolve database path %s: %w", path, err)
|
return "", "", fmt.Errorf("resolve database path %s: %w", path, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return abs, source, nil
|
// Not filepath.Abs or filepath.Join: they clean the path, which
|
||||||
|
// drops each ".." together with the directory name before it. When
|
||||||
|
// that directory is a symlink, the cleaned path names another file.
|
||||||
|
// The working directory ends in "/" only when it is the root.
|
||||||
|
return strings.TrimSuffix(wd, "/") + "/" + path, source, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// scanParams are the connection parameters for scan: read-write, with
|
// scanParams are the connection parameters for scan: read-write, with
|
||||||
|
|||||||
+16
-2
@@ -45,7 +45,8 @@ func openTestDB(t *testing.T) *sql.DB {
|
|||||||
//nolint:paralleltest // changes the process-wide working directory
|
//nolint:paralleltest // changes the process-wide working directory
|
||||||
func TestDatabasePath(t *testing.T) {
|
func TestDatabasePath(t *testing.T) {
|
||||||
// README §Database: SFDUPES_DATABASE when set and non-empty, the
|
// README §Database: SFDUPES_DATABASE when set and non-empty, the
|
||||||
// default otherwise, made absolute against the working directory.
|
// default otherwise, with the working directory put in front of a
|
||||||
|
// relative path. Nothing is cleaned, so a ".." stays.
|
||||||
dir := t.TempDir()
|
dir := t.TempDir()
|
||||||
t.Chdir(dir)
|
t.Chdir(dir)
|
||||||
|
|
||||||
@@ -55,7 +56,10 @@ func TestDatabasePath(t *testing.T) {
|
|||||||
}{
|
}{
|
||||||
{"", defaultDatabasePath, "default"},
|
{"", defaultDatabasePath, "default"},
|
||||||
{"/custom/place.sqlite", "/custom/place.sqlite", fromEnv},
|
{"/custom/place.sqlite", "/custom/place.sqlite", fromEnv},
|
||||||
{"sub/place.sqlite", filepath.Join(dir, "sub/place.sqlite"), fromEnv},
|
{"/custom/link/../place.sqlite", "/custom/link/../place.sqlite",
|
||||||
|
fromEnv},
|
||||||
|
{"sub/place.sqlite", dir + "/sub/place.sqlite", fromEnv},
|
||||||
|
{"link/../place.sqlite", dir + "/link/../place.sqlite", fromEnv},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range cases {
|
for _, tc := range cases {
|
||||||
@@ -67,6 +71,16 @@ func TestDatabasePath(t *testing.T) {
|
|||||||
databaseEnv, tc.env, path, source, err, tc.path, tc.source)
|
databaseEnv, tc.env, path, source, err, tc.path, tc.source)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// In the root directory the path gets one slash in front, not two.
|
||||||
|
t.Chdir("/")
|
||||||
|
t.Setenv(databaseEnv, "place.sqlite")
|
||||||
|
|
||||||
|
path, _, err := databasePath()
|
||||||
|
if err != nil || path != "/place.sqlite" {
|
||||||
|
t.Errorf("databasePath() in / = %q, %v; want %q",
|
||||||
|
path, err, "/place.sqlite")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestOpenScanDatabaseCreates(t *testing.T) {
|
func TestOpenScanDatabaseCreates(t *testing.T) {
|
||||||
|
|||||||
@@ -352,6 +352,72 @@ func TestRunNamesRelativeDatabase(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRunUsesDatabaseThroughSymlinkedDirectory(t *testing.T) {
|
||||||
|
// README §Database: the path names the file exactly. With link a
|
||||||
|
// symlink to the directory real/sub, link/../db.sqlite names
|
||||||
|
// real/db.sqlite, not db.sqlite beside link. Every subcommand must
|
||||||
|
// use that file, relative or absolute, and its first line must name
|
||||||
|
// the path as set, with the working directory put in front of a
|
||||||
|
// relative one.
|
||||||
|
dir := t.TempDir()
|
||||||
|
t.Chdir(dir)
|
||||||
|
|
||||||
|
err := os.MkdirAll(filepath.Join(dir, "real", "sub"), 0o750)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = os.Symlink(filepath.Join(dir, "real", "sub"),
|
||||||
|
filepath.Join(dir, "link"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
named := dir + "/link/../db.sqlite"
|
||||||
|
from := " (from " + databaseEnv + ")\n"
|
||||||
|
tree := t.TempDir()
|
||||||
|
|
||||||
|
t.Setenv(databaseEnv, "link/../db.sqlite")
|
||||||
|
|
||||||
|
want := "scan: new database " + named + from
|
||||||
|
if got := scanOK(t, tree); !strings.HasPrefix(got, want) {
|
||||||
|
t.Errorf("relative scan stderr = %q, want it to start %q", got, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
// The absolute path names the database the relative one created.
|
||||||
|
t.Setenv(databaseEnv, named)
|
||||||
|
|
||||||
|
want = "scan: database " + named + from
|
||||||
|
if got := scanOK(t, tree); !strings.HasPrefix(got, want) {
|
||||||
|
t.Errorf("absolute scan stderr = %q, want it to start %q", got, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, name := range []string{cmdReport, cmdTrees} {
|
||||||
|
stderr := captureStderr(t)
|
||||||
|
|
||||||
|
code := run([]string{name}, io.Discard, os.Stderr)
|
||||||
|
|
||||||
|
want = name + ": database " + named + from
|
||||||
|
if got := stderr(); code != exitOK || !strings.HasPrefix(got, want) {
|
||||||
|
t.Errorf("run(%s) = %d with stderr %q, want %d and it to "+
|
||||||
|
"start %q", name, code, got, exitOK, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = os.Stat(filepath.Join(dir, "real", "db.sqlite"))
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("database the path names: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// With no file at the cleaned path, report and trees succeeded on
|
||||||
|
// the database in real.
|
||||||
|
_, err = os.Stat(filepath.Join(dir, "db.sqlite"))
|
||||||
|
if !errors.Is(err, fs.ErrNotExist) {
|
||||||
|
t.Errorf("database at the cleaned path: stat error %v, want it "+
|
||||||
|
"missing", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestRunNamesDefaultDatabase(t *testing.T) {
|
func TestRunNamesDefaultDatabase(t *testing.T) {
|
||||||
// README §Database: with SFDUPES_DATABASE empty, each subcommand's
|
// README §Database: with SFDUPES_DATABASE empty, each subcommand's
|
||||||
// first line on stderr names the default database. A database may
|
// first line on stderr names the default database. A database may
|
||||||
|
|||||||
Reference in New Issue
Block a user