diff --git a/docs/development-windows.md b/docs/development-windows.md
new file mode 100644
index 000000000..5c4096cd9
--- /dev/null
+++ b/docs/development-windows.md
@@ -0,0 +1,90 @@
+# Development
+
+Memos is built with a curated tech stack. It is optimized for developer experience and is very easy to start working on the code:
+
+1. It has no external dependency.
+2. It requires zero config.
+3. 1 command to start backend and 1 command to start frontend, both with live reload support.
+
+## Tech Stack
+
+| Frontend | Backend |
+| ---------------------------------------- | --------------------------------- |
+| [React](https://react.dev/) | [Go](https://go.dev/) |
+| [Tailwind CSS](https://tailwindcss.com/) | [SQLite](https://www.sqlite.org/) |
+| [Vite](https://vitejs.dev/) | |
+| [pnpm](https://pnpm.io/) | |
+
+## Prerequisites
+
+- [Go](https://golang.org/doc/install)
+- [Air](https://github.com/cosmtrek/air#installation) for backend live reload
+- [Node.js](https://nodejs.org/)
+- [pnpm](https://pnpm.io/installation)
+
+## Steps
+
+(Using PowerShell)
+
+1. pull source code
+
+ ```powershell
+ git clone https://github.com/usememos/memos
+ # or
+ gh repo clone usememos/memos
+ ```
+
+2. cd into the project root directory
+
+ ```powershell
+ cd memos
+ ```
+
+3. start backend using air (with live reload)
+
+ ```powershell
+ air -c .\scripts\.air-windows.toml
+ ```
+
+4. start frontend dev server
+
+ ```powershell
+ cd web; pnpm i; pnpm dev
+ ```
+
+Memos should now be running at [http://localhost:3001](http://localhost:3001) and changing either frontend or backend code would trigger live reload.
+
+## Building
+
+Frontend must be built before backend. The built frontend must be placed in the backend ./server/dist directory. Otherwise, you will get a "No frontend embeded" error.
+
+### Frontend
+
+```powershell
+Move-Item "./server/dist" "./server/dist.bak"
+cd web; pnpm i --frozen-lockfile; pnpm build; cd ..;
+Move-Item "./web/dist" "./server/" -Force
+```
+
+### Backend
+
+```powershell
+go build -o ./build/memos.exe ./main.go
+```
+
+## ❕ Notes
+
+- Start development servers easier by running the provided `start.ps1` script.
+This will start both backend and frontend in detached PowerShell windows:
+
+ ```powershell
+ .\scripts\start.ps1
+ ```
+
+- Produce a local build easier using the provided `build.ps1` script to build both frontend and backend:
+
+ ```powershell
+ .\scripts\build.ps1
+ ```
+
+ This will produce a memos.exe file in the ./build directory.
diff --git a/docs/windows-service.md b/docs/windows-service.md
new file mode 100644
index 000000000..831cf6143
--- /dev/null
+++ b/docs/windows-service.md
@@ -0,0 +1,98 @@
+# Installing memos as a service on Windows
+
+While memos first-class support is for Docker, you may also install memos as a Windows service. It will run under SYSTEM account and start automatically at system boot.
+
+❗ All service management methods requires admin privileges. Use [gsudo](https://gerardog.github.io/gsudo/docs/install), or open a new PowerShell terminal as admin:
+
+```powershell
+Start-Process powershell -Verb RunAs
+```
+
+## Choose one of the following methods
+
+### 1. Using [NSSM](https://nssm.cc/download)
+
+NSSM is a lightweight service wrapper.
+
+You may put `nssm.exe` in the same directory as `memos.exe`, or add its directory to your system PATH. Prefer the latest 64-bit version of `nssm.exe`.
+
+```powershell
+# Install memos as a service
+nssm install memos "C:\path\to\memos.exe" --mode prod --port 5230
+
+# Delay auto start
+nssm set memos DisplayName "memos service"
+
+# Configure extra service parameters
+nssm set memos Description "A lightweight, self-hosted memo hub. https://usememos.com/"
+
+# Delay auto start
+nssm set memos Start SERVICE_DELAYED_AUTO_START
+
+# Edit service using NSSM GUI
+nssm edit memos
+
+# Start the service
+nssm start memos
+
+# Remove the service, if ever needed
+nssm remove memos confirm
+```
+
+### 2. Using [WinSW](https://github.com/winsw/winsw)
+
+Find the latest release tag and download the asset `WinSW-net46x.exe`. Then, put it in the same directory as `memos.exe` and rename it to `memos-service.exe`.
+
+Now, in the same directory, create the service configuration file `memos-service.xml`:
+
+```xml
+
+ memos
+ memos service
+ A lightweight, self-hosted memo hub. https://usememos.com/
+
+ %BASE%\memos.exe
+ --mode prod --port 5230
+ true
+
+
+```
+
+Then, install the service:
+
+```powershell
+# Install the service
+.\memos-service.exe install
+
+# Start the service
+.\memos-service.exe start
+
+# Remove the service, if ever needed
+.\memos-service.exe uninstall
+```
+
+### Manage the service
+
+You may use the `net` command to manage the service:
+
+```powershell
+net start memos
+net stop memos
+```
+
+Also, by using one of the provided methods, the service will appear in the Windows Services Manager `services.msc`.
+
+## Notes
+
+- On Windows, memos store its data in the following directory:
+
+ ```powershell
+ $env:ProgramData\memos
+ # Typically, this will resolve to C:\ProgramData\memos
+ ```
+
+ You may specify a custom directory by appending `--data ` to the service command line.
+
+- If the service fails to start, you should inspect the Windows Event Viewer `eventvwr.msc`.
+
+- Memos will be accessible at [http://localhost:5230](http://localhost:5230) by default.
diff --git a/scripts/.air-windows.toml b/scripts/.air-windows.toml
new file mode 100644
index 000000000..a8346c9c4
--- /dev/null
+++ b/scripts/.air-windows.toml
@@ -0,0 +1,15 @@
+root = "."
+tmp_dir = ".air"
+
+[build]
+ bin = "./.air/memos.exe"
+ cmd = "go build -o ./.air/memos.exe ./main.go"
+ delay = 1000
+ exclude_dir = [".air", "web", "build"]
+ exclude_file = []
+ exclude_regex = []
+ exclude_unchanged = false
+ follow_symlink = false
+ full_bin = ""
+ send_interrupt = true
+ kill_delay = 2000
diff --git a/scripts/build.ps1 b/scripts/build.ps1
new file mode 100644
index 000000000..e01688e84
--- /dev/null
+++ b/scripts/build.ps1
@@ -0,0 +1,36 @@
+# Usage: ./scripts/build.ps1
+# This is only for local builds.
+
+# For development, setup a proper environment as described here:
+# https://github.com/usememos/memos/blob/main/docs/development.md
+
+$projectRoot = (Resolve-Path "$MyInvocation.MyCommand.Path/..").Path
+Write-Host "Project root: $projectRoot"
+
+Write-Host "Building frontend..." -f Magenta
+Set-Location "$projectRoot/web"
+npm install -g pnpm
+pnpm i --frozen-lockfile
+pnpm build
+
+Write-Host "Backing up frontend placeholder..." -f Magenta
+Move-Item "$projectRoot/server/dist" "$projectRoot/server/dist.bak" -Force -ErrorAction Stop
+
+Write-Host "Moving frontend build to /server/dist ..." -f Magenta
+Move-Item "$projectRoot/web/dist" "$projectRoot/server/" -Force -ErrorAction Stop
+
+Set-Location $projectRoot
+
+Write-Host "Building backend..." -f Magenta
+go build -o ./build/memos.exe ./main.go
+Write-Host "Backend built!" -f green
+
+Write-Host "Removing frontend from /server/dist ..." -f Magenta
+Remove-Item "$projectRoot/server/dist" -Recurse -Force -ErrorAction SilentlyContinue
+
+Write-Host "Restoring frontend placeholder..." -f Magenta
+Move-Item "$projectRoot/server/dist.bak" "$projectRoot/server/dist" -Force -ErrorAction Stop
+
+Write-Host "You can test the build with ./build/memos.exe --mode demo" -f Green
+
+Set-Location -Path $projectRoot
\ No newline at end of file
diff --git a/scripts/start.ps1 b/scripts/start.ps1
new file mode 100644
index 000000000..fa2dfabc0
--- /dev/null
+++ b/scripts/start.ps1
@@ -0,0 +1,42 @@
+# This script starts the backend and frontend in development mode, with live reload.
+# It also installs frontend dependencies.
+
+# For more details on setting-up a development environment, check the docs:
+# https://github.com/usememos/memos/blob/main/docs/development.md
+
+# Usage: ./scripts/start.ps1
+$LastExitCode = 0
+
+$projectRoot = (Resolve-Path "$MyInvocation.MyCommand.Path/..").Path
+Write-Host "Project root: $projectRoot"
+
+Write-Host "Starting backend..." -f Magenta
+Start-Process -WorkingDirectory "$projectRoot" -FilePath "air" "-c ./scripts/.air-windows.toml"
+if ($LastExitCode -ne 0) {
+ Write-Host "Failed to start backend!" -f Red
+ exit $LastExitCode
+}
+else {
+ Write-Host "Backend started!" -f Green
+}
+
+Write-Host "Installing frontend dependencies..." -f Magenta
+Start-Process -Wait -WorkingDirectory "$projectRoot/web" -FilePath "powershell" -ArgumentList "pnpm i"
+if ($LastExitCode -ne 0) {
+ Write-Host "Failed to install frontend dependencies!" -f Red
+ exit $LastExitCode
+}
+else {
+ Write-Host "Frontend dependencies installed!" -f Green
+}
+
+Write-Host "Starting frontend..." -f Magenta
+Start-Process -WorkingDirectory "$projectRoot/web" -FilePath "powershell" -ArgumentList "pnpm dev"
+if ($LastExitCode -ne 0) {
+ Write-Host "Failed to start frontend!" -f Red
+ exit $LastExitCode
+}
+else {
+ Write-Host "Frontend started!" -f Green
+}
+
diff --git a/server/profile/profile.go b/server/profile/profile.go
index 1d0541b61..1c0df4398 100644
--- a/server/profile/profile.go
+++ b/server/profile/profile.go
@@ -4,6 +4,7 @@ import (
"fmt"
"os"
"path/filepath"
+ "runtime"
"strings"
"github.com/spf13/viper"
@@ -31,15 +32,16 @@ func (p *Profile) IsDev() bool {
func checkDSN(dataDir string) (string, error) {
// Convert to absolute path if relative path is supplied.
if !filepath.IsAbs(dataDir) {
- absDir, err := filepath.Abs(filepath.Dir(os.Args[0]) + "/" + dataDir)
+ relativeDir := filepath.Join(filepath.Dir(os.Args[0]), dataDir)
+ absDir, err := filepath.Abs(relativeDir)
if err != nil {
return "", err
}
dataDir = absDir
}
- // Trim trailing / in case user supplies
- dataDir = strings.TrimRight(dataDir, "/")
+ // Trim trailing \ or / in case user supplies
+ dataDir = strings.TrimRight(dataDir, "\\/")
if _, err := os.Stat(dataDir); err != nil {
return "", fmt.Errorf("unable to access data folder %s, err %w", dataDir, err)
@@ -61,7 +63,18 @@ func GetProfile() (*Profile, error) {
}
if profile.Mode == "prod" && profile.Data == "" {
- profile.Data = "/var/opt/memos"
+ if runtime.GOOS == "windows" {
+ profile.Data = filepath.Join(os.Getenv("ProgramData"), "memos")
+
+ if _, err := os.Stat(profile.Data); os.IsNotExist(err) {
+ if err := os.MkdirAll(profile.Data, 0770); err != nil {
+ fmt.Printf("Failed to create data directory: %s, err: %+v\n", profile.Data, err)
+ return nil, err
+ }
+ }
+ } else {
+ profile.Data = "/var/opt/memos"
+ }
}
dataDir, err := checkDSN(profile.Data)
@@ -71,7 +84,8 @@ func GetProfile() (*Profile, error) {
}
profile.Data = dataDir
- profile.DSN = fmt.Sprintf("%s/memos_%s.db", dataDir, profile.Mode)
+ dbFile := fmt.Sprintf("memos_%s.db", profile.Mode)
+ profile.DSN = filepath.Join(dataDir, dbFile)
profile.Version = version.GetCurrentVersion(profile.Mode)
return &profile, nil