mirror of
https://github.com/usememos/memos.git
synced 2024-10-04 23:38:46 +03:00
feat: add preliminar Windows support (#1636)
Add preliminar Windows support for both development and production environments. Default profile.Data will be set to "C:\ProgramData\memos" on Windows. Folder will be created if it does not exist, as this behavior is expected for Windows applications. System service installation can be achieved with third-party tools, explained in docs/windows-service.md. Not sure if it's worth using https://github.com/kardianos/service to make service support built-in. This could be a nice addition alongside #1583 (add Windows artifacts)
This commit is contained in:
parent
4605349bdc
commit
3b76c6792c
90
docs/development-windows.md
Normal file
90
docs/development-windows.md
Normal file
@ -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.
|
98
docs/windows-service.md
Normal file
98
docs/windows-service.md
Normal file
@ -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
|
||||||
|
<service>
|
||||||
|
<id>memos</id>
|
||||||
|
<name>memos service</name>
|
||||||
|
<description>A lightweight, self-hosted memo hub. https://usememos.com/</description>
|
||||||
|
<onfailure action="restart" delay="10 sec"/>
|
||||||
|
<executable>%BASE%\memos.exe</executable>
|
||||||
|
<arguments>--mode prod --port 5230</arguments>
|
||||||
|
<delayedAutoStart>true</delayedAutoStart>
|
||||||
|
<log mode="none" />
|
||||||
|
</service>
|
||||||
|
```
|
||||||
|
|
||||||
|
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 <path>` 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.
|
15
scripts/.air-windows.toml
Normal file
15
scripts/.air-windows.toml
Normal file
@ -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
|
36
scripts/build.ps1
Normal file
36
scripts/build.ps1
Normal file
@ -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
|
42
scripts/start.ps1
Normal file
42
scripts/start.ps1
Normal file
@ -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
|
||||||
|
}
|
||||||
|
|
@ -4,6 +4,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
@ -31,15 +32,16 @@ func (p *Profile) IsDev() bool {
|
|||||||
func checkDSN(dataDir string) (string, error) {
|
func checkDSN(dataDir string) (string, error) {
|
||||||
// Convert to absolute path if relative path is supplied.
|
// Convert to absolute path if relative path is supplied.
|
||||||
if !filepath.IsAbs(dataDir) {
|
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 {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
dataDir = absDir
|
dataDir = absDir
|
||||||
}
|
}
|
||||||
|
|
||||||
// Trim trailing / in case user supplies
|
// Trim trailing \ or / in case user supplies
|
||||||
dataDir = strings.TrimRight(dataDir, "/")
|
dataDir = strings.TrimRight(dataDir, "\\/")
|
||||||
|
|
||||||
if _, err := os.Stat(dataDir); err != nil {
|
if _, err := os.Stat(dataDir); err != nil {
|
||||||
return "", fmt.Errorf("unable to access data folder %s, err %w", dataDir, err)
|
return "", fmt.Errorf("unable to access data folder %s, err %w", dataDir, err)
|
||||||
@ -61,8 +63,19 @@ func GetProfile() (*Profile, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if profile.Mode == "prod" && profile.Data == "" {
|
if profile.Mode == "prod" && profile.Data == "" {
|
||||||
|
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"
|
profile.Data = "/var/opt/memos"
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
dataDir, err := checkDSN(profile.Data)
|
dataDir, err := checkDSN(profile.Data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -71,7 +84,8 @@ func GetProfile() (*Profile, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
profile.Data = dataDir
|
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)
|
profile.Version = version.GetCurrentVersion(profile.Mode)
|
||||||
|
|
||||||
return &profile, nil
|
return &profile, nil
|
||||||
|
Loading…
Reference in New Issue
Block a user