mirror of
https://github.com/usememos/memos.git
synced 2025-12-18 06:41:32 +08:00
Merge 1bc5336084 into 228cc6105d
This commit is contained in:
commit
68ff0c2aff
9 changed files with 248 additions and 29 deletions
72
README.md
72
README.md
|
|
@ -106,6 +106,78 @@ Access Memos at `http://localhost:5230` and complete the initial setup.
|
||||||
|
|
||||||
**Pro Tip**: The data directory stores all your notes, uploads, and settings. Include it in your backup strategy!
|
**Pro Tip**: The data directory stores all your notes, uploads, and settings. Include it in your backup strategy!
|
||||||
|
|
||||||
|
### 🔒 Database Encryption (for SQLite)
|
||||||
|
|
||||||
|
<details>
|
||||||
|
|
||||||
|
Memos can protect its SQLite database with **SQLCipher** so that the on-disk file is unreadable without a passphrase. This is *encryption at rest*: the server keeps the key in memory while running, so it does not provide end-to-end encryption for clients.
|
||||||
|
|
||||||
|
> [!IMPORTANT]
|
||||||
|
> Losing the passphrase means losing your data. Store it safely (for example, in a password manager or a hardware secret vault).
|
||||||
|
|
||||||
|
#### Enable SQLCipher Builds
|
||||||
|
|
||||||
|
- **Docker (recommended)**
|
||||||
|
```bash
|
||||||
|
docker build \
|
||||||
|
--build-arg CGO_ENABLED=1 \
|
||||||
|
--build-arg MEMOS_BUILD_TAGS="memos_sqlcipher libsqlite3 sqlite_omit_load_extension" \
|
||||||
|
-t memos-sqlcipher \
|
||||||
|
-f scripts/Dockerfile .
|
||||||
|
docker run -d \
|
||||||
|
--name memos \
|
||||||
|
-p 5230:5230 \
|
||||||
|
-v ~/.memos:/var/opt/memos \
|
||||||
|
-e MEMOS_SQLITE_ENCRYPTION_KEY="your-super-secret-key" \
|
||||||
|
memos-sqlcipher
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Manual build**
|
||||||
|
```bash
|
||||||
|
CGO_ENABLED=1 \
|
||||||
|
CGO_CFLAGS="-I/usr/include/sqlcipher -DSQLITE_HAS_CODEC" \
|
||||||
|
CGO_LDFLAGS="-lsqlcipher" \
|
||||||
|
go build -tags "memos_sqlcipher libsqlite3 sqlite_omit_load_extension" -o memos-sqlcipher ./bin/memos
|
||||||
|
./memos-sqlcipher --sqlite-encryption-key "your-super-secret-key" ...
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Migration Plan for Existing Deployments
|
||||||
|
|
||||||
|
1. **Full backup**
|
||||||
|
```bash
|
||||||
|
cp ~/.memos/memos_prod.db ~/.memos/memos_prod.db.bak
|
||||||
|
cp ~/.memos/memos_prod.db-wal ~/.memos/memos_prod.db-wal.bak 2>/dev/null || true
|
||||||
|
cp ~/.memos/memos_prod.db-shm ~/.memos/memos_prod.db-shm.bak 2>/dev/null || true
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Stop every Memos instance** touching the database.
|
||||||
|
|
||||||
|
3. **Build the SQLCipher-capable binary or Docker image** using the instructions above. The resulting image already contains the `sqlcipher` CLI.
|
||||||
|
|
||||||
|
4. **Convert the database** using the SQLCipher CLI. You can do this without installing anything on the host:
|
||||||
|
```bash
|
||||||
|
docker run --rm \
|
||||||
|
-v ~/.memos:/data \
|
||||||
|
memos-sqlcipher \
|
||||||
|
sh -c "cd /data && sqlcipher memos_prod.db <<'EOS'\nATTACH DATABASE 'memos_encrypted.db' AS encrypted KEY 'your-super-secret-key';\nSELECT sqlcipher_export('encrypted');\nDETACH DATABASE encrypted;\nEOS"
|
||||||
|
```
|
||||||
|
If you prefer to run the command directly on the host, install `sqlcipher` (e.g. `brew install sqlcipher`, `apt install sqlcipher`) and execute the same `ATTACH ... sqlcipher_export` sequence locally.
|
||||||
|
|
||||||
|
6. **Swap the files**
|
||||||
|
```bash
|
||||||
|
mv memos_prod.db memos_prod.db.plaintext
|
||||||
|
mv memos_encrypted.db memos_prod.db
|
||||||
|
rm -f memos_prod.db-wal memos_prod.db-shm
|
||||||
|
```
|
||||||
|
|
||||||
|
7. **Start the SQLCipher build of Memos** and pass the same key (`MEMOS_SQLITE_ENCRYPTION_KEY` or `--sqlite-encryption-key`).
|
||||||
|
|
||||||
|
8. **Verify the upgrade**
|
||||||
|
- Log in and ensure your memos/attachments are intact.
|
||||||
|
- Confirm the file is encrypted: `sqlite3 memos_prod.db '.tables'` should now print `Error: file is not a database`.
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
## Sponsors
|
## Sponsors
|
||||||
|
|
||||||
Memos is made possible by the generous support of our sponsors. Their contributions help ensure the project's continued development, maintenance, and growth.
|
Memos is made possible by the generous support of our sponsors. Their contributions help ensure the project's continued development, maintenance, and growth.
|
||||||
|
|
|
||||||
|
|
@ -25,15 +25,16 @@ var (
|
||||||
Short: `An open source, lightweight note-taking service. Easily capture and share your great thoughts.`,
|
Short: `An open source, lightweight note-taking service. Easily capture and share your great thoughts.`,
|
||||||
Run: func(_ *cobra.Command, _ []string) {
|
Run: func(_ *cobra.Command, _ []string) {
|
||||||
instanceProfile := &profile.Profile{
|
instanceProfile := &profile.Profile{
|
||||||
Mode: viper.GetString("mode"),
|
Mode: viper.GetString("mode"),
|
||||||
Addr: viper.GetString("addr"),
|
Addr: viper.GetString("addr"),
|
||||||
Port: viper.GetInt("port"),
|
Port: viper.GetInt("port"),
|
||||||
UNIXSock: viper.GetString("unix-sock"),
|
UNIXSock: viper.GetString("unix-sock"),
|
||||||
Data: viper.GetString("data"),
|
Data: viper.GetString("data"),
|
||||||
Driver: viper.GetString("driver"),
|
Driver: viper.GetString("driver"),
|
||||||
DSN: viper.GetString("dsn"),
|
DSN: viper.GetString("dsn"),
|
||||||
InstanceURL: viper.GetString("instance-url"),
|
SQLiteEncryptionKey: viper.GetString("sqlite-encryption-key"),
|
||||||
Version: version.GetCurrentVersion(viper.GetString("mode")),
|
InstanceURL: viper.GetString("instance-url"),
|
||||||
|
Version: version.GetCurrentVersion(viper.GetString("mode")),
|
||||||
}
|
}
|
||||||
if err := instanceProfile.Validate(); err != nil {
|
if err := instanceProfile.Validate(); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
|
|
@ -100,6 +101,7 @@ func init() {
|
||||||
rootCmd.PersistentFlags().String("data", "", "data directory")
|
rootCmd.PersistentFlags().String("data", "", "data directory")
|
||||||
rootCmd.PersistentFlags().String("driver", "sqlite", "database driver")
|
rootCmd.PersistentFlags().String("driver", "sqlite", "database driver")
|
||||||
rootCmd.PersistentFlags().String("dsn", "", "database source name(aka. DSN)")
|
rootCmd.PersistentFlags().String("dsn", "", "database source name(aka. DSN)")
|
||||||
|
rootCmd.PersistentFlags().String("sqlite-encryption-key", "", "SQLCipher key used to unlock the SQLite database (requires binary built with memos_sqlcipher)")
|
||||||
rootCmd.PersistentFlags().String("instance-url", "", "the url of your memos instance")
|
rootCmd.PersistentFlags().String("instance-url", "", "the url of your memos instance")
|
||||||
|
|
||||||
if err := viper.BindPFlag("mode", rootCmd.PersistentFlags().Lookup("mode")); err != nil {
|
if err := viper.BindPFlag("mode", rootCmd.PersistentFlags().Lookup("mode")); err != nil {
|
||||||
|
|
@ -123,6 +125,9 @@ func init() {
|
||||||
if err := viper.BindPFlag("dsn", rootCmd.PersistentFlags().Lookup("dsn")); err != nil {
|
if err := viper.BindPFlag("dsn", rootCmd.PersistentFlags().Lookup("dsn")); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
if err := viper.BindPFlag("sqlite-encryption-key", rootCmd.PersistentFlags().Lookup("sqlite-encryption-key")); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
if err := viper.BindPFlag("instance-url", rootCmd.PersistentFlags().Lookup("instance-url")); err != nil {
|
if err := viper.BindPFlag("instance-url", rootCmd.PersistentFlags().Lookup("instance-url")); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
@ -132,6 +137,9 @@ func init() {
|
||||||
if err := viper.BindEnv("instance-url", "MEMOS_INSTANCE_URL"); err != nil {
|
if err := viper.BindEnv("instance-url", "MEMOS_INSTANCE_URL"); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
if err := viper.BindEnv("sqlite-encryption-key", "MEMOS_SQLITE_ENCRYPTION_KEY"); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func printGreetings(profile *profile.Profile) {
|
func printGreetings(profile *profile.Profile) {
|
||||||
|
|
|
||||||
1
go.mod
1
go.mod
|
|
@ -19,6 +19,7 @@ require (
|
||||||
github.com/labstack/echo/v4 v4.13.4
|
github.com/labstack/echo/v4 v4.13.4
|
||||||
github.com/lib/pq v1.10.9
|
github.com/lib/pq v1.10.9
|
||||||
github.com/lithammer/shortuuid/v4 v4.2.0
|
github.com/lithammer/shortuuid/v4 v4.2.0
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.32
|
||||||
github.com/pkg/errors v0.9.1
|
github.com/pkg/errors v0.9.1
|
||||||
github.com/spf13/cobra v1.10.1
|
github.com/spf13/cobra v1.10.1
|
||||||
github.com/spf13/viper v1.20.1
|
github.com/spf13/viper v1.20.1
|
||||||
|
|
|
||||||
2
go.sum
2
go.sum
|
|
@ -285,6 +285,8 @@ github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Ky
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
|
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||||
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
|
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
|
||||||
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
|
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,8 @@ type Profile struct {
|
||||||
// Driver is the database driver
|
// Driver is the database driver
|
||||||
// sqlite, mysql
|
// sqlite, mysql
|
||||||
Driver string
|
Driver string
|
||||||
|
// SQLiteEncryptionKey unlocks SQLCipher-protected SQLite databases when provided.
|
||||||
|
SQLiteEncryptionKey string
|
||||||
// Version is the current version of server
|
// Version is the current version of server
|
||||||
Version string
|
Version string
|
||||||
// InstanceURL is the url of your memos instance.
|
// InstanceURL is the url of your memos instance.
|
||||||
|
|
@ -88,5 +90,9 @@ func (p *Profile) Validate() error {
|
||||||
p.DSN = filepath.Join(dataDir, dbFile)
|
p.DSN = filepath.Join(dataDir, dbFile)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if p.SQLiteEncryptionKey != "" && p.Driver != "sqlite" {
|
||||||
|
return errors.New("sqlite encryption key is only supported when using the sqlite driver")
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,12 @@
|
||||||
FROM golang:1.25-alpine AS backend
|
FROM golang:1.25-alpine AS backend
|
||||||
|
ARG MEMOS_BUILD_TAGS=""
|
||||||
|
ARG CGO_ENABLED=0
|
||||||
|
ARG CGO_CFLAGS=""
|
||||||
|
ARG CGO_LDFLAGS=""
|
||||||
|
ENV CGO_ENABLED=${CGO_ENABLED}
|
||||||
|
ENV MEMOS_BUILD_TAGS=${MEMOS_BUILD_TAGS}
|
||||||
|
ENV CGO_CFLAGS=${CGO_CFLAGS}
|
||||||
|
ENV CGO_LDFLAGS=${CGO_LDFLAGS}
|
||||||
WORKDIR /backend-build
|
WORKDIR /backend-build
|
||||||
COPY go.mod go.sum ./
|
COPY go.mod go.sum ./
|
||||||
RUN go mod download
|
RUN go mod download
|
||||||
|
|
@ -7,13 +15,50 @@ COPY . .
|
||||||
# Refer to `pnpm release` in package.json for the build command.
|
# Refer to `pnpm release` in package.json for the build command.
|
||||||
RUN --mount=type=cache,target=/go/pkg/mod \
|
RUN --mount=type=cache,target=/go/pkg/mod \
|
||||||
--mount=type=cache,target=/root/.cache/go-build \
|
--mount=type=cache,target=/root/.cache/go-build \
|
||||||
go build -ldflags="-s -w" -o memos ./bin/memos/main.go
|
/bin/sh -eux <<'EOF'
|
||||||
|
if [ "${CGO_ENABLED}" = "1" ]; then
|
||||||
|
apk add --no-cache --virtual .build-deps build-base pkgconf
|
||||||
|
if printf "%s" "${MEMOS_BUILD_TAGS}" | grep -q "memos_sqlcipher"; then
|
||||||
|
apk add --no-cache --virtual .sqlcipher-build sqlcipher-dev
|
||||||
|
SQLCIPHER_CFLAGS="$(pkg-config --cflags sqlcipher)"
|
||||||
|
SQLCIPHER_LDFLAGS="$(pkg-config --libs sqlcipher)"
|
||||||
|
if [ ! -e /usr/lib/libsqlite3.so ]; then
|
||||||
|
ln -s /usr/lib/libsqlcipher.so /usr/lib/libsqlite3.so
|
||||||
|
fi
|
||||||
|
if [ -z "${CGO_CFLAGS}" ]; then
|
||||||
|
export CGO_CFLAGS="${SQLCIPHER_CFLAGS} -DSQLITE_HAS_CODEC"
|
||||||
|
else
|
||||||
|
export CGO_CFLAGS="${CGO_CFLAGS} ${SQLCIPHER_CFLAGS} -DSQLITE_HAS_CODEC"
|
||||||
|
fi
|
||||||
|
if [ -z "${CGO_LDFLAGS}" ]; then
|
||||||
|
export CGO_LDFLAGS="${SQLCIPHER_LDFLAGS}"
|
||||||
|
else
|
||||||
|
export CGO_LDFLAGS="${CGO_LDFLAGS} ${SQLCIPHER_LDFLAGS}"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
go build -ldflags="-s -w" -tags="${MEMOS_BUILD_TAGS}" -o memos ./bin/memos/main.go
|
||||||
|
|
||||||
|
if [ "${CGO_ENABLED}" = "1" ]; then
|
||||||
|
if apk info -e .sqlcipher-build >/dev/null 2>&1; then
|
||||||
|
apk del .sqlcipher-build
|
||||||
|
fi
|
||||||
|
apk del .build-deps
|
||||||
|
fi
|
||||||
|
EOF
|
||||||
|
|
||||||
# Make workspace with above generated files.
|
# Make workspace with above generated files.
|
||||||
FROM alpine:latest AS monolithic
|
FROM alpine:latest AS monolithic
|
||||||
|
ARG MEMOS_BUILD_TAGS=""
|
||||||
WORKDIR /usr/local/memos
|
WORKDIR /usr/local/memos
|
||||||
|
|
||||||
RUN apk add --no-cache tzdata
|
RUN apk add --no-cache tzdata
|
||||||
|
RUN if printf "%s" "$MEMOS_BUILD_TAGS" | grep -q "memos_sqlcipher"; then \
|
||||||
|
apk add --no-cache sqlcipher sqlcipher-libs && \
|
||||||
|
if [ -e /usr/lib/libsqlcipher.so ]; then ln -sf /usr/lib/libsqlcipher.so /usr/lib/libsqlite3.so; fi && \
|
||||||
|
if [ -e /usr/lib/libsqlcipher.so.0 ]; then ln -sf /usr/lib/libsqlcipher.so.0 /usr/lib/libsqlcipher.so; fi; \
|
||||||
|
fi
|
||||||
ENV TZ="UTC"
|
ENV TZ="UTC"
|
||||||
|
|
||||||
COPY --from=backend /backend-build/memos /usr/local/memos/
|
COPY --from=backend /backend-build/memos /usr/local/memos/
|
||||||
|
|
|
||||||
32
store/db/sqlite/sqlcipher_disabled.go
Normal file
32
store/db/sqlite/sqlcipher_disabled.go
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
//go:build !memos_sqlcipher
|
||||||
|
|
||||||
|
package sqlite
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
|
"github.com/usememos/memos/internal/profile"
|
||||||
|
|
||||||
|
// Import the pure-Go SQLite driver.
|
||||||
|
_ "modernc.org/sqlite"
|
||||||
|
)
|
||||||
|
|
||||||
|
func openSQLiteDB(profile *profile.Profile) (*sql.DB, error) {
|
||||||
|
if profile.SQLiteEncryptionKey != "" {
|
||||||
|
return nil, errors.New("sqlite encryption key provided but binary is not built with SQLCipher support; rebuild with -tags memos_sqlcipher")
|
||||||
|
}
|
||||||
|
|
||||||
|
sqliteDB, err := sql.Open(sqliteModernDriver, profile.DSN)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrapf(err, "failed to open db with dsn: %s", profile.DSN)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := configureSQLiteConnection(sqliteDB); err != nil {
|
||||||
|
sqliteDB.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return sqliteDB, nil
|
||||||
|
}
|
||||||
49
store/db/sqlite/sqlcipher_enabled.go
Normal file
49
store/db/sqlite/sqlcipher_enabled.go
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
//go:build memos_sqlcipher
|
||||||
|
|
||||||
|
package sqlite
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
|
"github.com/usememos/memos/internal/profile"
|
||||||
|
|
||||||
|
// Import the CGO-backed SQLCipher-compatible SQLite driver.
|
||||||
|
_ "github.com/mattn/go-sqlite3"
|
||||||
|
)
|
||||||
|
|
||||||
|
func openSQLiteDB(profile *profile.Profile) (*sql.DB, error) {
|
||||||
|
sqliteDB, err := sql.Open(sqliteCipherDriver, profile.DSN)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrapf(err, "failed to open db with dsn: %s", profile.DSN)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := applySQLiteEncryptionKey(sqliteDB, profile.SQLiteEncryptionKey); err != nil {
|
||||||
|
sqliteDB.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := configureSQLiteConnection(sqliteDB); err != nil {
|
||||||
|
sqliteDB.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return sqliteDB, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func applySQLiteEncryptionKey(db *sql.DB, key string) error {
|
||||||
|
if key == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
escapedKey := strings.ReplaceAll(key, "'", "''")
|
||||||
|
pragma := fmt.Sprintf("PRAGMA key = '%s'", escapedKey)
|
||||||
|
if _, err := db.Exec(pragma); err != nil {
|
||||||
|
return errors.Wrap(err, "failed to apply sqlite encryption key; verify the binary is linked against SQLCipher")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
@ -3,12 +3,10 @@ package sqlite
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
// Import the SQLite driver.
|
|
||||||
_ "modernc.org/sqlite"
|
|
||||||
|
|
||||||
"github.com/usememos/memos/internal/profile"
|
"github.com/usememos/memos/internal/profile"
|
||||||
"github.com/usememos/memos/store"
|
"github.com/usememos/memos/store"
|
||||||
)
|
)
|
||||||
|
|
@ -21,29 +19,21 @@ type DB struct {
|
||||||
// NewDB opens a database specified by its database driver name and a
|
// NewDB opens a database specified by its database driver name and a
|
||||||
// driver-specific data source name, usually consisting of at least a
|
// driver-specific data source name, usually consisting of at least a
|
||||||
// database name and connection information.
|
// database name and connection information.
|
||||||
|
const (
|
||||||
|
sqliteBusyTimeout = 10000
|
||||||
|
sqliteModernDriver = "sqlite"
|
||||||
|
sqliteCipherDriver = "sqlite3"
|
||||||
|
)
|
||||||
|
|
||||||
func NewDB(profile *profile.Profile) (store.Driver, error) {
|
func NewDB(profile *profile.Profile) (store.Driver, error) {
|
||||||
// Ensure a DSN is set before attempting to open the database.
|
// Ensure a DSN is set before attempting to open the database.
|
||||||
if profile.DSN == "" {
|
if profile.DSN == "" {
|
||||||
return nil, errors.New("dsn required")
|
return nil, errors.New("dsn required")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Connect to the database with some sane settings:
|
sqliteDB, err := openSQLiteDB(profile)
|
||||||
// - No shared-cache: it's obsolete; WAL journal mode is a better solution.
|
|
||||||
// - No foreign key constraints: it's currently disabled by default, but it's a
|
|
||||||
// good practice to be explicit and prevent future surprises on SQLite upgrades.
|
|
||||||
// - Journal mode set to WAL: it's the recommended journal mode for most applications
|
|
||||||
// as it prevents locking issues.
|
|
||||||
//
|
|
||||||
// Notes:
|
|
||||||
// - When using the `modernc.org/sqlite` driver, each pragma must be prefixed with `_pragma=`.
|
|
||||||
//
|
|
||||||
// References:
|
|
||||||
// - https://pkg.go.dev/modernc.org/sqlite#Driver.Open
|
|
||||||
// - https://www.sqlite.org/sharedcache.html
|
|
||||||
// - https://www.sqlite.org/pragma.html
|
|
||||||
sqliteDB, err := sql.Open("sqlite", profile.DSN+"?_pragma=foreign_keys(0)&_pragma=busy_timeout(10000)&_pragma=journal_mode(WAL)")
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrapf(err, "failed to open db with dsn: %s", profile.DSN)
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
driver := DB{db: sqliteDB, profile: profile}
|
driver := DB{db: sqliteDB, profile: profile}
|
||||||
|
|
@ -51,6 +41,20 @@ func NewDB(profile *profile.Profile) (store.Driver, error) {
|
||||||
return &driver, nil
|
return &driver, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func configureSQLiteConnection(db *sql.DB) error {
|
||||||
|
pragmas := []string{
|
||||||
|
"PRAGMA foreign_keys = OFF",
|
||||||
|
fmt.Sprintf("PRAGMA busy_timeout = %d", sqliteBusyTimeout),
|
||||||
|
"PRAGMA journal_mode = WAL",
|
||||||
|
}
|
||||||
|
for _, pragma := range pragmas {
|
||||||
|
if _, err := db.Exec(pragma); err != nil {
|
||||||
|
return errors.Wrapf(err, "failed to execute %s", pragma)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (d *DB) GetDB() *sql.DB {
|
func (d *DB) GetDB() *sql.DB {
|
||||||
return d.db
|
return d.db
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue