Compare commits

..

1 Commits

Author SHA1 Message Date
boojack eea826dd5c chore: release v0.2.2 2022-07-22 23:42:26 +08:00
284 changed files with 5224 additions and 13759 deletions
-1
View File
@@ -1 +0,0 @@
ko_fi: stevenlgtm
-31
View File
@@ -1,31 +0,0 @@
name: Bug Report
description: Create a report to help us improve
labels: [bug]
body:
- type: markdown
attributes:
value: |
If you are reporting a new issue, make sure that we do not have any duplicates already open. You can ensure this by searching the issue list for this repository. If there is a duplicate, please close your issue and add a comment to the existing issue instead.
- type: textarea
attributes:
label: Describe the bug
description: |
Briefly describe the problem you are having in a few paragraphs.
validations:
required: true
- type: textarea
attributes:
label: Steps to reproduce
description: |
Provide the steps to reproduce the issue.
placeholder: |
1. Go to '...'
2. Click on '....'
3. See error
validations:
required: true
- type: textarea
attributes:
label: Screenshots or additional context
description: |
Add screenshots or any other context about the problem.
@@ -1,28 +0,0 @@
name: Feature Request
description: Suggest an idea for this project
labels: [enhancement]
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to suggest an idea for Memos!
- type: textarea
attributes:
label: Is your feature request related to a problem?
description: |
A clear and concise description of what the problem is.
placeholder: |
I'm always frustrated when [...]
validations:
required: true
- type: textarea
attributes:
label: Describe the solution you'd like
description: |
A clear and concise description of what you want to happen.
validations:
required: true
- type: textarea
attributes:
label: Additional context
description: Add any other context or screenshots about the feature request.
@@ -0,0 +1,37 @@
name: build-and-push-dev-image
on:
push:
branches:
- "main"
jobs:
build-and-push-dev-image:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: neosmemo
password: ${{ secrets.DOCKER_NEOSMEMO_TOKEN }}
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v2
with:
install: true
- name: Build and Push
id: docker_build
uses: docker/build-push-action@v3
with:
context: ./
file: ./Dockerfile
platforms: linux/amd64,linux/arm64
push: true
tags: neosmemo/memos:dev
@@ -4,7 +4,7 @@ on:
push:
branches:
# Run on pushing branches like `release/1.0.0`
- "release/*.*.*"
- "release/v*.*.*"
jobs:
build-and-push-release-image:
@@ -17,9 +17,9 @@ jobs:
- name: Extract build args
# Extract version from branch name
# Example: branch name `release/1.0.0` sets up env.VERSION=1.0.0
# Example: branch name `release/v1.0.0` sets up env.VERSION=1.0.0
run: |
echo "VERSION=${GITHUB_REF_NAME#release/}" >> $GITHUB_ENV
echo "VERSION=${GITHUB_REF_NAME#release/v}" >> $GITHUB_ENV
- name: Login to Docker Hub
uses: docker/login-action@v2
@@ -2,10 +2,11 @@ name: build-and-push-test-image
on:
push:
branches: [main]
branches:
- "test/*"
jobs:
build-and-push-test-image:
build-and-push-dev-image:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
@@ -31,6 +32,6 @@ jobs:
with:
context: ./
file: ./Dockerfile
platforms: linux/amd64
platforms: linux/amd64,linux/arm64
push: true
tags: neosmemo/memos:test
+30 -28
View File
@@ -13,10 +13,12 @@ name: "CodeQL"
on:
push:
branches: [main]
branches: [ main ]
pull_request:
# The branches below must be a subset of the branches above
branches: [main]
branches: [ main ]
schedule:
- cron: '27 12 * * 0'
jobs:
analyze:
@@ -30,39 +32,39 @@ jobs:
strategy:
fail-fast: false
matrix:
language: ["go", "javascript"]
language: [ 'go', 'javascript' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
# Learn more about CodeQL language support at https://git.io/codeql-language-support
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Checkout repository
uses: actions/checkout@v2
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v1
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v1
# ️ Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# ️ Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
#- run: |
# make bootstrap
# make release
#- run: |
# make bootstrap
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1
-88
View File
@@ -1,88 +0,0 @@
name: Test
on:
push:
branches:
- main
- "release/v*.*.*"
pull_request:
branches: [main]
jobs:
eslint-checks:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: "18"
cache: yarn
cache-dependency-path: "web/yarn.lock"
- run: yarn
working-directory: web
- name: Run eslint check
run: yarn lint
working-directory: web
jest-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: "18"
cache: yarn
cache-dependency-path: "web/yarn.lock"
- run: yarn
working-directory: web
- name: Run jest
run: yarn test
working-directory: web
frontend-build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: "18"
cache: yarn
cache-dependency-path: "web/yarn.lock"
- run: yarn
working-directory: web
- name: Run frontend build
run: yarn build
working-directory: web
go-static-checks:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v3
with:
go-version: 1.19
check-latest: true
cache: true
- name: Verify go.mod is tidy
run: |
go mod tidy -go=1.19
git diff --exit-code
- name: golangci-lint
uses: golangci/golangci-lint-action@v3
with:
args: -v
skip-cache: true
go-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v3
with:
go-version: 1.19
check-latest: true
cache: true
- name: Run all tests
run: go test -v ./... | tee test.log; exit ${PIPESTATUS[0]}
- name: Pretty print tests running time
run: grep --color=never -e '--- PASS:' -e '--- FAIL:' test.log | sed 's/[:()]//g' | awk '{print $2,$3,$4}' | sort -t' ' -nk3 -r | awk '{sum += $3; print $1,$2,$3,sum"s"}'
+2 -10
View File
@@ -1,18 +1,10 @@
# Air (hot reload) generated
.air
# temp folder
tmp
# Frontend asset
web/dist
# build folder
build
memos-build
.DS_Store
# Jetbrains
.idea
bin/air
.DS_Store
-64
View File
@@ -1,64 +0,0 @@
linters:
enable:
- goimports
- revive
- govet
- staticcheck
- misspell
- gocritic
- sqlclosecheck
- rowserrcheck
- nilerr
- godot
issues:
exclude:
- Rollback
- fmt.Printf
- fmt.Print
linters-settings:
revive:
enable-all-rules: true
rules:
- name: file-header
disabled: true
- name: line-length-limit
disabled: true
- name: function-length
disabled: true
- name: max-public-structs
disabled: true
- name: function-result-limit
disabled: true
- name: banned-characters
disabled: true
- name: argument-limit
disabled: true
- name: cognitive-complexity
disabled: true
- name: cyclomatic
disabled: true
- name: confusing-results
disabled: true
- name: add-constant
disabled: true
- name: flag-parameter
disabled: true
- name: nested-structs
disabled: true
- name: import-shadowing
disabled: true
- name: early-return
disabled: true
gocritic:
disabled-checks:
- ifElseChain
govet:
settings:
printf:
funcs:
- common.Errorf
forbidigo:
forbid:
- 'fmt\.Errorf(# Please use errors\.Wrap\|Wrapf\|Errorf instead)?'
-4
View File
@@ -1,4 +0,0 @@
{
"go.lintOnSave": "workspace",
"go.lintTool": "golangci-lint"
}
+7 -5
View File
@@ -1,5 +1,5 @@
# Build frontend dist.
FROM node:18.12.1-alpine3.16 AS frontend
FROM node:16.15.0-alpine AS frontend
WORKDIR /frontend-build
COPY ./web/ .
@@ -8,7 +8,7 @@ RUN yarn
RUN yarn build
# Build backend exec file.
FROM golang:1.19.3-alpine3.16 AS backend
FROM golang:1.18.3-alpine3.16 AS backend
WORKDIR /backend-build
RUN apk update
@@ -17,10 +17,12 @@ RUN apk --no-cache add gcc musl-dev
COPY . .
COPY --from=frontend /frontend-build/dist ./server/dist
RUN go build -o memos ./bin/server/main.go
RUN go build \
-o memos \
./bin/server/main.go
# Make workspace with above generated files.
FROM alpine:3.16 AS monolithic
FROM alpine:3.16.0 AS monolithic
WORKDIR /usr/local/memos
COPY --from=backend /backend-build/memos /usr/local/memos/
@@ -28,4 +30,4 @@ COPY --from=backend /backend-build/memos /usr/local/memos/
# Directory to store the data, which can be referenced as the mounting point.
RUN mkdir -p /var/opt/memos
ENTRYPOINT ["./memos", "--mode", "prod", "--port", "5230"]
ENTRYPOINT ["./memos"]
+57 -33
View File
@@ -1,65 +1,89 @@
<p align="center"><a href="https://usememos.com"><img height="64px" src="https://raw.githubusercontent.com/usememos/memos/main/resources/logo-full.webp" alt="✍️ memos" /></a></p>
<h1 align="center">✍️ Memos</h1>
<p align="center">An open-source, self-hosted memo hub with knowledge management and collaboration.</p>
<p align="center">An open source, self-hosted knowledge base that works with a SQLite db file.</p>
<p align="center">
<a href="https://github.com/usememos/memos/stargazers"><img alt="GitHub stars" src="https://img.shields.io/github/stars/usememos/memos" /></a>
<img alt="GitHub stars" src="https://img.shields.io/github/stars/usememos/memos" />
<a href="https://hub.docker.com/r/neosmemo/memos"><img alt="Docker pull" src="https://img.shields.io/docker/pulls/neosmemo/memos.svg" /></a>
<img alt="Go report" src="https://goreportcard.com/badge/github.com/usememos/memos" />
</p>
<p align="center">
<a href="https://demo.usememos.com/">Live Demo</a> •
Discuss in <a href="https://t.me/+-_tNF1k70UU4ZTc9">Telegram</a> / <b><a href="https://discord.gg/tfPJa4UmAv">Discord 🏂</a></b>
<a href="https://memos.onrender.com/">Live Demo</a> •
<a href="https://t.me/+-_tNF1k70UU4ZTc9">Discuss in Telegram 👾</a>
</p>
![demo](https://raw.githubusercontent.com/usememos/memos/main/resources/demo.webp)
## Features
## Features
- 🦄 Open source and free forever;
- 🚀 Support for self-hosting with `Docker` in seconds;
- 📜 Plain textarea first and support some useful markdown syntax;
- 👥 Collaborate and share with your teammates;
- 🧑‍💻 RESTful API for self-service.
- 🦄 Fully open source;
- 📜 Writing in plain textarea without any burden,
- and support some useful markdown syntax 💪.
- 🌄 Share the memo in a pretty image or personal page like Twitter;
- 🚀 Fast self-hosting with `Docker`;
- 🤠 Pleasant UI and UX;
## Deploy with Docker in seconds
### Docker Run
## ⚓️ Deploy with Docker
```docker
docker run -d --name memos -p 5230:5230 -v ~/.memos/:/var/opt/memos neosmemo/memos:latest
docker run \
--name memos \
--publish 5230:5230 \
--volume ~/.memos/:/var/opt/memos \
neosmemo/memos:latest \
--mode prod \
--port 5230
```
Memos should be running at [http://localhost:5230](http://localhost:5230). If the `~/.memos/` does not have a `memos_prod.db` file, then memos will auto generate it.
Memos should now be running at [http://localhost:5230](http://localhost:5230). If the `~/.memos/` does not have a `memos_prod.db` file, then `memos` will auto generate it.
### Docker Compose
⚠️ Please DO NOT use `dev` tag of docker image if you have no experience.
Example Compose YAML file: [`docker-compose.yaml`](./docker-compose.yaml).
## 🏗 Development
If you want to upgrade the version of memos, use the following command.
Memos is built with a curated tech stack. It is optimized for developer experience and is very easy to start working on the code:
```sh
docker-compose down && docker image rm neosmemo/memos:latest && docker-compose up -d
```
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.
## Contribute
### Tech Stack
Contributions are what make the open source community such an amazing place to be learn, inspire, and create. Any contributions you make are greatly appreciated. 🥰
<img alt="tech stack" src="https://raw.githubusercontent.com/usememos/memos/main/resources/tech-stack.png" width="360" />
See more in [development guide](https://github.com/usememos/memos/tree/main/docs/development.md).
### Prerequisites
## Products made by Community
- [Go](https://golang.org/doc/install) (1.16 or later)
- [Air](https://github.com/cosmtrek/air#installation) for backend live reload
- [yarn](https://yarnpkg.com/getting-started/install)
- [Moe Memos](https://memos.moe/) - Third party client for iOS and Android
- [lmm214/memos-bber](https://github.com/lmm214/memos-bber) - Chrome extension
- [Rabithua/memos_wmp](https://github.com/Rabithua/memos_wmp) - Wechat miniprogram
- [qazxcdswe123/telegramMemoBot](https://github.com/qazxcdswe123/telegramMemoBot) - Telegram bot
### Steps
## License
1. pull source code
This project is open source and available under the [MIT License](https://github.com/usememos/memos/blob/main/LICENSE).
```bash
git clone https://github.com/usememos/memos
```
## Star history
2. start backend using air(with live reload)
```bash
air -c scripts/.air.toml
```
3. start frontend dev server
```bash
cd web && yarn && yarn dev
```
Memos should now be running at [http://localhost:3000](http://localhost:3000) and change either frontend or backend code would trigger live reload.
## 🌟 Star history
[![Star History Chart](https://api.star-history.com/svg?repos=usememos/memos&type=Date)](https://star-history.com/#usememos/memos&Date)
---
Just enjoy it.
-7
View File
@@ -1,7 +0,0 @@
# Security Policy
## Reporting a bug
Report security bugs via GitHub [issues](https://github.com/usememos/memos/issues).
For more information, please contact [stevenlgtm@gmail.com](stevenlgtm@gmail.com).
+4 -3
View File
@@ -1,12 +1,13 @@
package api
type Signin struct {
Username string `json:"username"`
Email string `json:"email"`
Password string `json:"password"`
}
type Signup struct {
Username string `json:"username"`
Password string `json:"password"`
Email string `json:"email"`
Role Role `json:"role"`
Name string `json:"name"`
Password string `json:"password"`
}
-22
View File
@@ -1,22 +0,0 @@
package api
// CacheNamespace is the type of a cache.
type CacheNamespace string
const (
// UserCache is the cache type of users.
UserCache CacheNamespace = "u"
// MemoCache is the cache type of memos.
MemoCache CacheNamespace = "m"
// ShortcutCache is the cache type of shortcuts.
ShortcutCache CacheNamespace = "s"
// ResourceCache is the cache type of resources.
ResourceCache CacheNamespace = "r"
)
// CacheService is the service for caches.
type CacheService interface {
FindCache(namespace CacheNamespace, id int, entry interface{}) (bool, error)
UpsertCache(namespace CacheNamespace, id int, entry interface{}) error
DeleteCache(namespace CacheNamespace, id int)
}
+8 -23
View File
@@ -6,8 +6,6 @@ type Visibility string
const (
// Public is the PUBLIC visibility.
Public Visibility = "PUBLIC"
// Protected is the PROTECTED visibility.
Protected Visibility = "PROTECTED"
// Privite is the PRIVATE visibility.
Privite Visibility = "PRIVATE"
)
@@ -16,8 +14,6 @@ func (e Visibility) String() string {
switch e {
case Public:
return "PUBLIC"
case Protected:
return "PROTECTED"
case Privite:
return "PRIVATE"
}
@@ -37,39 +33,28 @@ type Memo struct {
Content string `json:"content"`
Visibility Visibility `json:"visibility"`
Pinned bool `json:"pinned"`
DisplayTs int64 `json:"displayTs"`
// Related fields
Creator *User `json:"creator"`
ResourceList []*Resource `json:"resourceList"`
}
type MemoCreate struct {
// Standard fields
CreatorID int
// Used to import memos with a clearly created ts.
CreatedTs *int64 `json:"createdTs"`
// Domain specific fields
Visibility Visibility `json:"visibility"`
Content string `json:"content"`
// Related fields
ResourceIDList []int `json:"resourceIdList"`
Content string `json:"content"`
Visibility *Visibility `json:"visibility"`
}
type MemoPatch struct {
ID int
// Standard fields
CreatedTs *int64 `json:"createdTs"`
UpdatedTs *int64
RowStatus *RowStatus `json:"rowStatus"`
// Domain specific fields
Content *string `json:"content"`
Visibility *Visibility `json:"visibility"`
// Related fields
ResourceIDList []int `json:"resourceIdList"`
}
type MemoFind struct {
@@ -80,9 +65,9 @@ type MemoFind struct {
CreatorID *int `json:"creatorId"`
// Domain specific fields
Pinned *bool
ContentSearch *string
VisibilityList []Visibility
Pinned *bool
ContentSearch *string
Visibility *Visibility
// Pagination
Limit int
@@ -90,5 +75,5 @@ type MemoFind struct {
}
type MemoDelete struct {
ID int
ID int `json:"id"`
}
-5
View File
@@ -19,8 +19,3 @@ type MemoOrganizerUpsert struct {
UserID int
Pinned bool `json:"pinned"`
}
type MemoOrganizerDelete struct {
MemoID *int
UserID *int
}
-24
View File
@@ -1,24 +0,0 @@
package api
type MemoResource struct {
MemoID int
ResourceID int
CreatedTs int64
UpdatedTs int64
}
type MemoResourceUpsert struct {
MemoID int
ResourceID int
UpdatedTs *int64
}
type MemoResourceFind struct {
MemoID *int
ResourceID *int
}
type MemoResourceDelete struct {
MemoID *int
ResourceID *int
}
+1 -15
View File
@@ -10,12 +10,9 @@ type Resource struct {
// Domain specific fields
Filename string `json:"filename"`
Blob []byte `json:"-"`
Blob []byte `json:"blob"`
Type string `json:"type"`
Size int64 `json:"size"`
// Related fields
LinkedMemoAmount int `json:"linkedMemoAmount"`
}
type ResourceCreate struct {
@@ -35,17 +32,6 @@ type ResourceFind struct {
// Standard fields
CreatorID *int `json:"creatorId"`
// Domain specific fields
Filename *string `json:"filename"`
MemoID *int
}
type ResourcePatch struct {
ID int
// Standard fields
UpdatedTs *int64
// Domain specific fields
Filename *string `json:"filename"`
}
+1 -5
View File
@@ -27,7 +27,6 @@ type ShortcutPatch struct {
ID int
// Standard fields
UpdatedTs *int64
RowStatus *RowStatus `json:"rowStatus"`
// Domain specific fields
@@ -46,8 +45,5 @@ type ShortcutFind struct {
}
type ShortcutDelete struct {
ID *int
// Standard fields
CreatorID *int
ID int
}
+2 -11
View File
@@ -3,15 +3,6 @@ package api
import "github.com/usememos/memos/server/profile"
type SystemStatus struct {
Host *User `json:"host"`
Profile profile.Profile `json:"profile"`
DBSize int64 `json:"dbSize"`
// System settings
// Allow sign up.
AllowSignUp bool `json:"allowSignUp"`
// Additional style.
AdditionalStyle string `json:"additionalStyle"`
// Additional script.
AdditionalScript string `json:"additionalScript"`
Host *User `json:"host"`
Profile *profile.Profile `json:"profile"`
}
-87
View File
@@ -1,87 +0,0 @@
package api
import (
"encoding/json"
"fmt"
)
type SystemSettingName string
const (
// SystemSettingAllowSignUpName is the key type of allow signup setting.
SystemSettingAllowSignUpName SystemSettingName = "allowSignUp"
// SystemSettingAdditionalStyleName is the key type of additional style.
SystemSettingAdditionalStyleName SystemSettingName = "additionalStyle"
// SystemSettingAdditionalScriptName is the key type of additional script.
SystemSettingAdditionalScriptName SystemSettingName = "additionalScript"
)
func (key SystemSettingName) String() string {
switch key {
case SystemSettingAllowSignUpName:
return "allowSignUp"
case SystemSettingAdditionalStyleName:
return "additionalStyle"
case SystemSettingAdditionalScriptName:
return "additionalScript"
}
return ""
}
var (
SystemSettingAllowSignUpValue = []bool{true, false}
)
type SystemSetting struct {
Name SystemSettingName
// Value is a JSON string with basic value
Value string
Description string
}
type SystemSettingUpsert struct {
Name SystemSettingName `json:"name"`
Value string `json:"value"`
Description string `json:"description"`
}
func (upsert SystemSettingUpsert) Validate() error {
if upsert.Name == SystemSettingAllowSignUpName {
value := false
err := json.Unmarshal([]byte(upsert.Value), &value)
if err != nil {
return fmt.Errorf("failed to unmarshal system setting allow signup value")
}
invalid := true
for _, v := range SystemSettingAllowSignUpValue {
if value == v {
invalid = false
break
}
}
if invalid {
return fmt.Errorf("invalid system setting allow signup value")
}
} else if upsert.Name == SystemSettingAdditionalStyleName {
value := ""
err := json.Unmarshal([]byte(upsert.Value), &value)
if err != nil {
return fmt.Errorf("failed to unmarshal system setting additional style value")
}
} else if upsert.Name == SystemSettingAdditionalScriptName {
value := ""
err := json.Unmarshal([]byte(upsert.Value), &value)
if err != nil {
return fmt.Errorf("failed to unmarshal system setting additional script value")
}
} else {
return fmt.Errorf("invalid system setting name")
}
return nil
}
type SystemSettingFind struct {
Name *SystemSettingName `json:"name"`
}
+12 -41
View File
@@ -1,17 +1,11 @@
package api
import (
"fmt"
)
// Role is the type of a role.
type Role string
const (
// Host is the HOST role.
Host Role = "HOST"
// Admin is the ADMIN role.
Admin Role = "ADMIN"
// NormalUser is the USER role.
NormalUser Role = "USER"
)
@@ -20,8 +14,6 @@ func (e Role) String() string {
switch e {
case Host:
return "HOST"
case Admin:
return "ADMIN"
case NormalUser:
return "USER"
}
@@ -37,48 +29,32 @@ type User struct {
UpdatedTs int64 `json:"updatedTs"`
// Domain specific fields
Username string `json:"username"`
Role Role `json:"role"`
Email string `json:"email"`
Nickname string `json:"nickname"`
PasswordHash string `json:"-"`
OpenID string `json:"openId"`
UserSettingList []*UserSetting `json:"userSettingList"`
Email string `json:"email"`
Role Role `json:"role"`
Name string `json:"name"`
PasswordHash string `json:"-"`
OpenID string `json:"openId"`
}
type UserCreate struct {
// Domain specific fields
Username string `json:"username"`
Role Role `json:"role"`
Email string `json:"email"`
Nickname string `json:"nickname"`
Role Role `json:"role"`
Name string `json:"name"`
Password string `json:"password"`
PasswordHash string
OpenID string
}
func (create UserCreate) Validate() error {
if len(create.Username) < 4 {
return fmt.Errorf("username is too short, minimum length is 4")
}
if len(create.Password) < 4 {
return fmt.Errorf("password is too short, minimum length is 4")
}
return nil
}
type UserPatch struct {
ID int
// Standard fields
UpdatedTs *int64
RowStatus *RowStatus `json:"rowStatus"`
// Domain specific fields
Username *string `json:"username"`
Email *string `json:"email"`
Nickname *string `json:"nickname"`
Name *string `json:"name"`
Password *string `json:"password"`
ResetOpenID *bool `json:"resetOpenId"`
PasswordHash *string
@@ -92,13 +68,8 @@ type UserFind struct {
RowStatus *RowStatus `json:"rowStatus"`
// Domain specific fields
Username *string `json:"username"`
Role *Role
Email *string `json:"email"`
Nickname *string `json:"nickname"`
OpenID *string
}
type UserDelete struct {
ID int
Email *string `json:"email"`
Role *Role
Name *string `json:"name"`
OpenID *string
}
-119
View File
@@ -1,119 +0,0 @@
package api
import (
"encoding/json"
"fmt"
)
type UserSettingKey string
const (
// UserSettingLocaleKey is the key type for user locale.
UserSettingLocaleKey UserSettingKey = "locale"
// UserSettingMemoVisibilityKey is the key type for user preference memo default visibility.
UserSettingMemoVisibilityKey UserSettingKey = "memoVisibility"
// UserSettingMemoDisplayTsOptionKey is the key type for memo display ts option.
UserSettingMemoDisplayTsOptionKey UserSettingKey = "memoDisplayTsOption"
)
// String returns the string format of UserSettingKey type.
func (key UserSettingKey) String() string {
switch key {
case UserSettingLocaleKey:
return "locale"
case UserSettingMemoVisibilityKey:
return "memoVisibility"
case UserSettingMemoDisplayTsOptionKey:
return "memoDisplayTsOption"
}
return ""
}
var (
UserSettingLocaleValue = []string{"en", "zh", "vi", "fr"}
UserSettingMemoVisibilityValue = []Visibility{Privite, Protected, Public}
UserSettingEditorFontStyleValue = []string{"normal", "mono"}
UserSettingMemoDisplayTsOptionKeyValue = []string{"created_ts", "updated_ts"}
)
type UserSetting struct {
UserID int
Key UserSettingKey `json:"key"`
// Value is a JSON string with basic value
Value string `json:"value"`
}
type UserSettingUpsert struct {
UserID int
Key UserSettingKey `json:"key"`
Value string `json:"value"`
}
func (upsert UserSettingUpsert) Validate() error {
if upsert.Key == UserSettingLocaleKey {
localeValue := "en"
err := json.Unmarshal([]byte(upsert.Value), &localeValue)
if err != nil {
return fmt.Errorf("failed to unmarshal user setting locale value")
}
invalid := true
for _, value := range UserSettingLocaleValue {
if localeValue == value {
invalid = false
break
}
}
if invalid {
return fmt.Errorf("invalid user setting locale value")
}
} else if upsert.Key == UserSettingMemoVisibilityKey {
memoVisibilityValue := Privite
err := json.Unmarshal([]byte(upsert.Value), &memoVisibilityValue)
if err != nil {
return fmt.Errorf("failed to unmarshal user setting memo visibility value")
}
invalid := true
for _, value := range UserSettingMemoVisibilityValue {
if memoVisibilityValue == value {
invalid = false
break
}
}
if invalid {
return fmt.Errorf("invalid user setting memo visibility value")
}
} else if upsert.Key == UserSettingMemoDisplayTsOptionKey {
memoDisplayTsOption := "created_ts"
err := json.Unmarshal([]byte(upsert.Value), &memoDisplayTsOption)
if err != nil {
return fmt.Errorf("failed to unmarshal user setting memo display ts option")
}
invalid := true
for _, value := range UserSettingMemoDisplayTsOptionKeyValue {
if memoDisplayTsOption == value {
invalid = false
break
}
}
if invalid {
return fmt.Errorf("invalid user setting memo display ts option value")
}
} else {
return fmt.Errorf("invalid user setting key")
}
return nil
}
type UserSettingFind struct {
UserID int
Key *UserSettingKey `json:"key"`
}
type UserSettingDelete struct {
UserID int
}
+63
View File
@@ -0,0 +1,63 @@
package cmd
import (
"fmt"
"os"
"github.com/usememos/memos/server"
"github.com/usememos/memos/server/profile"
"github.com/usememos/memos/store"
DB "github.com/usememos/memos/store/db"
)
const (
greetingBanner = `
███╗ ███╗███████╗███╗ ███╗ ██████╗ ███████╗
████╗ ████║██╔════╝████╗ ████║██╔═══██╗██╔════╝
██╔████╔██║█████╗ ██╔████╔██║██║ ██║███████╗
██║╚██╔╝██║██╔══╝ ██║╚██╔╝██║██║ ██║╚════██║
██║ ╚═╝ ██║███████╗██║ ╚═╝ ██║╚██████╔╝███████║
╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝
`
)
type Main struct {
profile *profile.Profile
}
func (m *Main) Run() error {
db := DB.NewDB(m.profile)
if err := db.Open(); err != nil {
return fmt.Errorf("cannot open db: %w", err)
}
s := server.NewServer(m.profile)
storeInstance := store.New(db.Db, m.profile)
s.Store = storeInstance
println(greetingBanner)
fmt.Printf("Version %s has started at :%d\n", m.profile.Version, m.profile.Port)
return s.Run()
}
func Execute() {
profile := profile.GetProfile()
m := Main{
profile: profile,
}
println("---")
println("profile")
println("mode:", profile.Mode)
println("port:", profile.Port)
println("dsn:", profile.DSN)
println("version:", profile.Version)
println("---")
if err := m.Run(); err != nil {
fmt.Printf("error: %+v\n", err)
os.Exit(1)
}
}
+2 -75
View File
@@ -1,80 +1,7 @@
package main
import (
"os"
_ "github.com/mattn/go-sqlite3"
"context"
"fmt"
metric "github.com/usememos/memos/plugin/metrics"
"github.com/usememos/memos/server"
"github.com/usememos/memos/server/profile"
"github.com/usememos/memos/store"
DB "github.com/usememos/memos/store/db"
)
const (
greetingBanner = `
███╗ ███╗███████╗███╗ ███╗ ██████╗ ███████╗
████╗ ████║██╔════╝████╗ ████║██╔═══██╗██╔════╝
██╔████╔██║█████╗ ██╔████╔██║██║ ██║███████╗
██║╚██╔╝██║██╔══╝ ██║╚██╔╝██║██║ ██║╚════██║
██║ ╚═╝ ██║███████╗██║ ╚═╝ ██║╚██████╔╝███████║
╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝
`
)
func run(profile *profile.Profile) error {
ctx := context.Background()
db := DB.NewDB(profile)
if err := db.Open(ctx); err != nil {
return fmt.Errorf("cannot open db: %w", err)
}
serverInstance := server.NewServer(profile)
storeInstance := store.New(db.Db, profile)
serverInstance.Store = storeInstance
metricCollector := server.NewMetricCollector(profile, storeInstance)
serverInstance.Collector = &metricCollector
println(greetingBanner)
fmt.Printf("Version %s has started at :%d\n", profile.Version, profile.Port)
metricCollector.Collect(ctx, &metric.Metric{
Name: "service started",
})
return serverInstance.Run()
}
func execute() error {
profile, err := profile.GetProfile()
if err != nil {
return err
}
println("---")
println("profile")
println("mode:", profile.Mode)
println("port:", profile.Port)
println("dsn:", profile.DSN)
println("version:", profile.Version)
println("---")
if err := run(profile); err != nil {
fmt.Printf("error: %+v\n", err)
return err
}
return nil
}
import "github.com/usememos/memos/bin/server/cmd"
func main() {
if err := execute(); err != nil {
os.Exit(1)
}
cmd.Execute()
}
+6 -1
View File
@@ -9,7 +9,7 @@ type Code int
// Application error codes.
const (
// 0 ~ 99 general error.
// 0 ~ 99 general error
Ok Code = 0
Internal Code = 1
NotAuthorized Code = 2
@@ -17,6 +17,11 @@ const (
NotFound Code = 4
Conflict Code = 5
NotImplemented Code = 6
// 101 ~ 199 db error
DbConnectionFailure Code = 101
DbStatementSyntaxError Code = 102
DbExecutionError Code = 103
)
// Error represents an application-specific error. Application errors can be
-16
View File
@@ -1,7 +1,6 @@
package common
import (
"net/mail"
"strings"
"github.com/google/uuid"
@@ -17,21 +16,6 @@ func HasPrefixes(src string, prefixes ...string) bool {
return false
}
// ValidateEmail validates the email.
func ValidateEmail(email string) bool {
if _, err := mail.ParseAddress(email); err != nil {
return false
}
return true
}
func GenUUID() string {
return uuid.New().String()
}
func Min(x, y int) int {
if x < y {
return x
}
return y
}
-31
View File
@@ -1,31 +0,0 @@
package common
import (
"testing"
)
func TestValidateEmail(t *testing.T) {
tests := []struct {
email string
want bool
}{
{
email: "t@gmail.com",
want: true,
},
{
email: "@qq.com",
want: false,
},
{
email: "1@gmail",
want: true,
},
}
for _, test := range tests {
result := ValidateEmail(test.email)
if result != test.want {
t.Errorf("Validate Email %s: got result %v, want %v.", test.email, result, test.want)
}
}
}
@@ -1,4 +1,4 @@
package version
package common
import (
"strconv"
@@ -7,10 +7,10 @@ import (
// Version is the service current released version.
// Semantic versioning: https://semver.org/
var Version = "0.8.0"
var Version = "0.2.2"
// DevVersion is the service current development version.
var DevVersion = "0.8.0"
var DevVersion = "0.2.2"
func GetCurrentVersion(mode string) string {
if mode == "dev" {
@@ -27,12 +27,6 @@ func GetMinorVersion(version string) string {
return versionList[0] + "." + versionList[1]
}
func GetSchemaVersion(version string) string {
minorVersion := GetMinorVersion(version)
return minorVersion + ".0"
}
// convSemanticVersionToInt converts version string to int.
func convSemanticVersionToInt(version string) int {
versionList := strings.Split(version, ".")
-9
View File
@@ -1,9 +0,0 @@
version: "3.0"
services:
memos:
image: neosmemo/memos:latest
container_name: memos
volumes:
- ~/.memos/:/var/opt/memos
ports:
- 5230:5230
-40
View File
@@ -1,40 +0,0 @@
# 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
![tech-stack](https://raw.githubusercontent.com/usememos/memos/main/resources/tech-stack.png)
## Prerequisites
- [Go](https://golang.org/doc/install)
- [Air](https://github.com/cosmtrek/air#installation) for backend live reload
- [Node.js](https://nodejs.org/)
- [yarn](https://yarnpkg.com/getting-started/install)
## Steps
1. pull source code
```bash
git clone https://github.com/usememos/memos
```
2. start backend using air(with live reload)
```bash
air -c scripts/.air.toml
```
3. start frontend dev server
```bash
cd web && yarn && yarn dev
```
Memos should now be running at [http://localhost:3000](http://localhost:3000) and change either frontend or backend code would trigger live reload.
+17 -33
View File
@@ -1,48 +1,32 @@
module github.com/usememos/memos
go 1.19
go 1.17
require github.com/mattn/go-sqlite3 v1.14.9
require github.com/google/uuid v1.3.0
require (
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa
golang.org/x/net v0.0.0-20220728030405-41545e8bf201
)
require github.com/labstack/echo/v4 v4.9.0
require (
github.com/VictoriaMetrics/fastcache v1.10.0
github.com/gorilla/feeds v1.1.1
github.com/gorilla/securecookie v1.1.1
github.com/gorilla/sessions v1.2.1
github.com/labstack/echo-contrib v0.13.0
github.com/stretchr/testify v1.8.1
)
require (
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/gorilla/context v1.1.1 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/labstack/gommon v0.3.1 // indirect
github.com/mattn/go-colorable v0.1.12 // indirect
github.com/mattn/go-colorable v0.1.11 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/segmentio/backo-go v1.0.1 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.1 // indirect
github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c // indirect
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 // indirect
golang.org/x/crypto v0.0.0-20210920023735-84f357641f63
golang.org/x/net v0.0.0-20210917221730-978cfadd31cf // indirect
golang.org/x/sys v0.0.0-20211103235746-7861aae1554b // indirect
golang.org/x/text v0.3.7 // indirect
golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
golang.org/x/time v0.0.0-20201208040808-7e3f01d25324 // indirect
)
require github.com/segmentio/analytics-go v3.1.0+incompatible
require (
github.com/gorilla/context v1.1.1 // indirect
github.com/labstack/echo/v4 v4.6.3
github.com/labstack/gommon v0.3.1 // indirect
)
require (
github.com/gorilla/securecookie v1.1.1
github.com/gorilla/sessions v1.2.1
github.com/labstack/echo-contrib v0.12.0
)
+569 -47
View File
@@ -1,91 +1,613 @@
github.com/VictoriaMetrics/fastcache v1.10.0 h1:5hDJnLsKLpnUEToub7ETuRu8RCkb40woBZAUiKonXzY=
github.com/VictoriaMetrics/fastcache v1.10.0/go.mod h1:tjiYeEfYXCqacuvYw/7UoDIeJaNxq6132xHICNP77w8=
github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156 h1:eMwmnE/GDgah4HI848JfFxHt+iPb26b4zyfspmqY0/8=
github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156/go.mod h1:Cb/ax3seSYIx7SuZdm2G2xzfwmv3TPSk2ucNfQESPXM=
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY=
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/HdrHistogram/hdrhistogram-go v1.1.2/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo=
github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
github.com/Shopify/sarama v1.30.0/go.mod h1:zujlQQx1kzHsh4jfV1USnptCQrHAEZ2Hk8fTKCulPVs=
github.com/Shopify/toxiproxy/v2 v2.1.6-0.20210914104332-15ea381dcdae/go.mod h1:/cvHQkZ1fst0EmZnA5dFtiQdWCNCFYzb+uE2vqVgvx0=
github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
github.com/appleboy/gofight/v2 v2.1.2/go.mod h1:frW+U1QZEdDgixycTj4CygQ48yLTUhplt43+Wczp3rw=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/casbin/casbin/v2 v2.40.6/go.mod h1:sEL80qBYTbd+BPeL4iyvwYzFT3qwLaESq5aFKVLbLfA=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/eapache/go-resiliency v1.2.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
github.com/gorilla/feeds v1.1.1 h1:HwKXxqzcRNg9to+BbvJog4+f3s/xzvtZXICcQGutYfY=
github.com/gorilla/feeds v1.1.1/go.mod h1:Nk0jZrvPFZX1OBe5NPiddPw7CfwF6Q9eqzaBbaightA=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI=
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs=
github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM=
github.com/jcmturner/gofork v1.0.0/go.mod h1:MK8+TM0La+2rjBD4jE12Kj1pCCxK7d2LK/UM3ncEo0o=
github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg=
github.com/jcmturner/gokrb5/v8 v8.4.2/go.mod h1:sb+Xq/fTY5yktf/VxLsE3wlfPqQjp0aWNYyvBVK62bc=
github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/labstack/echo-contrib v0.13.0 h1:bzSG0SpuZZd7BmJLvsWtPfU23W0Enh3K0tok3aENVKA=
github.com/labstack/echo-contrib v0.13.0/go.mod h1:IF9+MJu22ADOZEHD+bAV67XMIO3vNXUy7Naz/ABPHEs=
github.com/labstack/echo/v4 v4.9.0 h1:wPOF1CE6gvt/kmbMR4dGzWvHMPT+sAEUJOwOTtvITVY=
github.com/labstack/echo/v4 v4.9.0/go.mod h1:xkCDAdFCIf8jsFQ5NnbK7oqaF/yU1A1X20Ltm0OvSks=
github.com/labstack/echo-contrib v0.12.0 h1:NPr1ez+XUa5s/4LujEon+32Bxg5DO6EKSW/va06pmLc=
github.com/labstack/echo-contrib v0.12.0/go.mod h1:kR62TbwsBgmpV2HVab5iQRsQtLuhPyGqCBee88XRc4M=
github.com/labstack/echo/v4 v4.6.1/go.mod h1:RnjgMWNDB9g/HucVWhQYNQP9PvbYf6adqftqryo7s9k=
github.com/labstack/echo/v4 v4.6.3 h1:VhPuIZYxsbPmo4m9KAkMU/el2442eB7EBFFhNTTT9ac=
github.com/labstack/echo/v4 v4.6.3/go.mod h1:Hk5OiHj0kDqmFq7aHe7eDqI7CUhuCrfpupQtLGGLm7A=
github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k=
github.com/labstack/gommon v0.3.1 h1:OomWaJXm7xR6L1HmEtGyQf26TEn7V6X88mktX9kee9o=
github.com/labstack/gommon v0.3.1/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.11 h1:nQ+aFkoE2TMGc0b68U2OKSexC+eq46+XwZzWXHRmPYs=
github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-sqlite3 v1.14.9 h1:10HX2Td0ocZpYEjhilsuo6WWtUqttj2Kb0KtD86/KYA=
github.com/mattn/go-sqlite3 v1.14.9/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/onsi/gomega v1.16.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc=
github.com/openzipkin/zipkin-go v0.3.0/go.mod h1:4c3sLeE8xjNqehmF5RpAFLPLJxXscc0R4l6Zg0P1tTQ=
github.com/pierrec/lz4 v2.6.1+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/segmentio/analytics-go v3.1.0+incompatible h1:IyiOfUgQFVHvsykKKbdI7ZsH374uv3/DfZUo9+G0Z80=
github.com/segmentio/analytics-go v3.1.0+incompatible/go.mod h1:C7CYBtQWk4vRk2RyLu0qOcbHJ18E3F1HV2C/8JvKN48=
github.com/segmentio/backo-go v1.0.1 h1:68RQccglxZeyURy93ASB/2kc9QudzgIDexJ927N++y4=
github.com/segmentio/backo-go v1.0.1/go.mod h1:9/Rh6yILuLysoQnZ2oNooD2g7aBnvM7r/fNVxRNWfBc=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=
github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/rabbitmq/amqp091-go v1.1.0/go.mod h1:ogQDLSOACsLPsIq0NpbtiifNZi2YOz0VTJ0kHRghqbM=
github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/uber/jaeger-client-go v2.30.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk=
github.com/uber/jaeger-lib v2.4.1+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U=
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
github.com/valyala/fasttemplate v1.2.1 h1:TVEnxayobAdVkhQfrfes2IzOB6o+z4roRkPF52WA1u4=
github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c h1:3lbZUMbMiGUW/LMkfsEABsc5zNT9+b1CvsJx47JzJ8g=
github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c/go.mod h1:UrdRz5enIKZ63MEE3IF9l2/ebyx59GyGgPi+tICQdmM=
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa h1:zuSxTR4o9y82ebqCUJYNGJbGPo6sKVl54f/TVDObg1c=
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/net v0.0.0-20220728030405-41545e8bf201 h1:bvOltf3SADAfG05iRml8lAB3qjoEX5RCyN4K6G5v3N0=
golang.org/x/net v0.0.0-20220728030405-41545e8bf201/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.0.2/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+hCSs=
github.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6+da4O5kxM=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201112155050-0c6587e931a9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210920023735-84f357641f63 h1:kETrAMYZq6WVGPa8IIixL0CaEcIUNi+1WX7grUoi3y8=
golang.org/x/crypto v0.0.0-20210920023735-84f357641f63/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210913180222-943fd674d43e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210917221730-978cfadd31cf h1:R150MpwJIv1MpS0N/pc+NhTM8ajzvlmxlY5OYsrevXQ=
golang.org/x/net v0.0.0-20210917221730-978cfadd31cf/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210910150752-751e447fb3d0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211103235746-7861aae1554b h1:1VkfZQv42XQlA/jchYumAnv1UPo6RgF9rJFkTgZIxO4=
golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220405052023-b1e9470b6e64/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 h1:WIoqL4EROvwiPdUtaip4VcDdpZ4kha7wBWZrbVKCIZg=
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9 h1:ftMN5LMiBFjbzleLqtoBZk7KdJwhuybIU+FckUHgoyQ=
golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20201208040808-7e3f01d25324 h1:Hir2P/De0WpUhtrKGGjvSb2YxUgyZ7EFOSLIcSSpiwE=
golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo=
gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0=
gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw=
gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.41.0/go.mod h1:U3l9uK9J0sini8mHphKoXyaqDA/8VyGnDee1zzIUK6k=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
-98
View File
@@ -1,98 +0,0 @@
package getter
import (
"fmt"
"io"
"net/http"
"net/url"
"golang.org/x/net/html"
"golang.org/x/net/html/atom"
)
type HTMLMeta struct {
Title string `json:"title"`
Description string `json:"description"`
Image string `json:"image"`
}
func GetHTMLMeta(urlStr string) (*HTMLMeta, error) {
if _, err := url.Parse(urlStr); err != nil {
return nil, err
}
response, err := http.Get(urlStr)
if err != nil {
return nil, err
}
defer response.Body.Close()
mediatype, err := getMediatype(response)
if err != nil {
return nil, err
}
if mediatype != "text/html" {
return nil, fmt.Errorf("Wrong website mediatype")
}
htmlMeta := extractHTMLMeta(response.Body)
return htmlMeta, nil
}
func extractHTMLMeta(resp io.Reader) *HTMLMeta {
tokenizer := html.NewTokenizer(resp)
htmlMeta := new(HTMLMeta)
for {
tokenType := tokenizer.Next()
if tokenType == html.ErrorToken {
break
} else if tokenType == html.StartTagToken || tokenType == html.SelfClosingTagToken {
token := tokenizer.Token()
if token.DataAtom == atom.Body {
break
}
if token.DataAtom == atom.Title {
tokenizer.Next()
token := tokenizer.Token()
htmlMeta.Title = token.Data
} else if token.DataAtom == atom.Meta {
description, ok := extractMetaProperty(token, "description")
if ok {
htmlMeta.Description = description
}
ogTitle, ok := extractMetaProperty(token, "og:title")
if ok {
htmlMeta.Title = ogTitle
}
ogDescription, ok := extractMetaProperty(token, "og:description")
if ok {
htmlMeta.Description = ogDescription
}
ogImage, ok := extractMetaProperty(token, "og:image")
if ok {
htmlMeta.Image = ogImage
}
}
}
}
return htmlMeta
}
func extractMetaProperty(token html.Token, prop string) (content string, ok bool) {
content, ok = "", false
for _, attr := range token.Attr {
if attr.Key == "property" && attr.Val == prop {
ok = true
}
if attr.Key == "content" {
content = attr.Val
}
}
return content, ok
}
-28
View File
@@ -1,28 +0,0 @@
package getter
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestGetHTMLMeta(t *testing.T) {
tests := []struct {
urlStr string
htmlMeta HTMLMeta
}{
{
urlStr: "https://www.bytebase.com/blog/sql-review-tool-for-devs",
htmlMeta: HTMLMeta{
Title: "The SQL Review Tool for Developers",
Description: "Reviewing SQL can be somewhat tedious, yet is essential to keep your database fleet reliable. At Bytebase, we are building a developer-first SQL review tool to empower the DevOps system.",
Image: "https://www.bytebase.com/static/blog/sql-review-tool-for-devs/dev-fighting-dba.webp",
},
},
}
for _, test := range tests {
metadata, err := GetHTMLMeta(test.urlStr)
require.NoError(t, err)
require.Equal(t, test.htmlMeta, *metadata)
}
}
-4
View File
@@ -1,4 +0,0 @@
// getter is using to get resources from url.
// * Get metadata for website;
// * Get image blob to avoid CORS;
package getter
-45
View File
@@ -1,45 +0,0 @@
package getter
import (
"fmt"
"io"
"net/http"
"net/url"
"strings"
)
type Image struct {
Blob []byte
Mediatype string
}
func GetImage(urlStr string) (*Image, error) {
if _, err := url.Parse(urlStr); err != nil {
return nil, err
}
response, err := http.Get(urlStr)
if err != nil {
return nil, err
}
defer response.Body.Close()
mediatype, err := getMediatype(response)
if err != nil {
return nil, err
}
if !strings.HasPrefix(mediatype, "image/") {
return nil, fmt.Errorf("Wrong image mediatype")
}
bodyBytes, err := io.ReadAll(response.Body)
if err != nil {
return nil, err
}
image := &Image{
Blob: bodyBytes,
Mediatype: mediatype,
}
return image, nil
}
-21
View File
@@ -1,21 +0,0 @@
package getter
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestGetImage(t *testing.T) {
tests := []struct {
urlStr string
}{
{
urlStr: "https://star-history.com/bytebase.webp",
},
}
for _, test := range tests {
_, err := GetImage(test.urlStr)
require.NoError(t, err)
}
}
-15
View File
@@ -1,15 +0,0 @@
package getter
import (
"mime"
"net/http"
)
func getMediatype(response *http.Response) (string, error) {
contentType := response.Header.Get("content-type")
mediatype, _, err := mime.ParseMediaType(contentType)
if err != nil {
return "", err
}
return mediatype, nil
}
-6
View File
@@ -1,6 +0,0 @@
package metric
// Collector is the interface definition for metric collector.
type Collector interface {
Collect(metric *Metric) error
}
-7
View File
@@ -1,7 +0,0 @@
package metric
// Metric is the API message for metric.
type Metric struct {
Name string
Labels map[string]string
}
-42
View File
@@ -1,42 +0,0 @@
package segment
import (
"time"
"github.com/google/uuid"
"github.com/segmentio/analytics-go"
metric "github.com/usememos/memos/plugin/metrics"
)
var (
sessionUUID = uuid.NewString()
)
// collector is the metrics collector https://segment.com/.
type collector struct {
client analytics.Client
}
// NewCollector creates a new instance of segment.
func NewCollector(key string) metric.Collector {
client := analytics.New(key)
return &collector{
client: client,
}
}
// Collect will exec all the segment collector.
func (c *collector) Collect(metric *metric.Metric) error {
properties := analytics.NewProperties()
for key, value := range metric.Labels {
properties.Set(key, value)
}
return c.client.Enqueue(analytics.Track{
Event: string(metric.Name),
AnonymousId: sessionUUID,
Properties: properties,
Timestamp: time.Now().UTC(),
})
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

+1 -1
View File
@@ -5,7 +5,7 @@ tmp_dir = ".air"
bin = "./.air/memos"
cmd = "go build -o ./.air/memos ./bin/server/main.go"
delay = 1000
exclude_dir = [".air", "web", "build"]
exclude_dir = [".air", "web"]
exclude_file = []
exclude_regex = []
exclude_unchanged = false
Executable → Regular
+4 -7
View File
@@ -1,13 +1,10 @@
#!/bin/bash
# Usage: ./scripts/build.sh
# Usage: sh ./scripts/build.sh
set -e
cd "$(dirname "$0")/../"
echo "Start building backend..."
echo "Start building..."
go build -o ./build/memos ./bin/server/main.go
go build -o ./memos-build/memos ./bin/server/main.go
echo "Backend built!"
echo "Build finished"
-9
View File
@@ -1,9 +0,0 @@
#!/bin/bash
# Usage: ./scripts/start.sh
set -e
cd "$(dirname "$0")/../"
air -c ./scripts/.air.toml
-121
View File
@@ -1,121 +0,0 @@
package server
import (
"fmt"
"net/http"
"strconv"
"github.com/usememos/memos/api"
"github.com/usememos/memos/common"
"github.com/gorilla/sessions"
"github.com/labstack/echo-contrib/session"
"github.com/labstack/echo/v4"
)
var (
userIDContextKey = "user-id"
)
func getUserIDContextKey() string {
return userIDContextKey
}
func setUserSession(ctx echo.Context, user *api.User) error {
sess, _ := session.Get("memos_session", ctx)
sess.Options = &sessions.Options{
Path: "/",
MaxAge: 3600 * 24 * 30,
HttpOnly: true,
}
sess.Values[userIDContextKey] = user.ID
err := sess.Save(ctx.Request(), ctx.Response())
if err != nil {
return fmt.Errorf("failed to set session, err: %w", err)
}
return nil
}
func removeUserSession(ctx echo.Context) error {
sess, _ := session.Get("memos_session", ctx)
sess.Options = &sessions.Options{
Path: "/",
MaxAge: 0,
HttpOnly: true,
}
sess.Values[userIDContextKey] = nil
err := sess.Save(ctx.Request(), ctx.Response())
if err != nil {
return fmt.Errorf("failed to set session, err: %w", err)
}
return nil
}
func aclMiddleware(s *Server, next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
ctx := c.Request().Context()
path := c.Path()
// Skip auth.
if common.HasPrefixes(path, "/api/auth") {
return next(c)
}
{
// If there is openId in query string and related user is found, then skip auth.
openID := c.QueryParam("openId")
if openID != "" {
userFind := &api.UserFind{
OpenID: &openID,
}
user, err := s.Store.FindUser(ctx, userFind)
if err != nil && common.ErrorCode(err) != common.NotFound {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user by open_id").SetInternal(err)
}
if user != nil {
// Stores userID into context.
c.Set(getUserIDContextKey(), user.ID)
return next(c)
}
}
}
{
sess, _ := session.Get("memos_session", c)
userIDValue := sess.Values[userIDContextKey]
if userIDValue != nil {
userID, _ := strconv.Atoi(fmt.Sprintf("%v", userIDValue))
userFind := &api.UserFind{
ID: &userID,
}
user, err := s.Store.FindUser(ctx, userFind)
if err != nil && common.ErrorCode(err) != common.NotFound {
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to find user by ID: %d", userID)).SetInternal(err)
}
if user != nil {
if user.RowStatus == api.Archived {
return echo.NewHTTPError(http.StatusForbidden, fmt.Sprintf("User has been archived with username %s", user.Username))
}
c.Set(getUserIDContextKey(), userID)
}
}
}
if common.HasPrefixes(path, "/api/ping", "/api/status", "/api/user/:id", "/api/memo/all", "/api/memo/:memoId", "/api/memo/amount") && c.Request().Method == http.MethodGet {
return next(c)
}
if common.HasPrefixes(path, "/api/memo", "/api/tag", "/api/shortcut") && c.Request().Method == http.MethodGet {
if _, err := strconv.Atoi(c.QueryParam("creatorId")); err == nil {
return next(c)
}
}
userID := c.Get(getUserIDContextKey())
if userID == nil {
return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
}
return next(c)
}
}
+33 -61
View File
@@ -7,7 +7,6 @@ import (
"github.com/usememos/memos/api"
"github.com/usememos/memos/common"
metric "github.com/usememos/memos/plugin/metrics"
"github.com/labstack/echo/v4"
"golang.org/x/crypto/bcrypt"
@@ -15,23 +14,22 @@ import (
func (s *Server) registerAuthRoutes(g *echo.Group) {
g.POST("/auth/signin", func(c echo.Context) error {
ctx := c.Request().Context()
signin := &api.Signin{}
if err := json.NewDecoder(c.Request().Body).Decode(signin); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted signin request").SetInternal(err)
}
userFind := &api.UserFind{
Username: &signin.Username,
Email: &signin.Email,
}
user, err := s.Store.FindUser(ctx, userFind)
if err != nil && common.ErrorCode(err) != common.NotFound {
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to find user by username %s", signin.Username)).SetInternal(err)
user, err := s.Store.FindUser(userFind)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to find user by email %s", signin.Email)).SetInternal(err)
}
if user == nil {
return echo.NewHTTPError(http.StatusUnauthorized, fmt.Sprintf("User not found with username %s", signin.Username))
return echo.NewHTTPError(http.StatusUnauthorized, fmt.Sprintf("User not found with email %s", signin.Email))
} else if user.RowStatus == api.Archived {
return echo.NewHTTPError(http.StatusForbidden, fmt.Sprintf("User has been archived with username %s", signin.Username))
return echo.NewHTTPError(http.StatusForbidden, fmt.Sprintf("User has been archived with email %s", signin.Email))
}
// Compare the stored hashed password, with the hashed version of the password that was received.
@@ -43,9 +41,6 @@ func (s *Server) registerAuthRoutes(g *echo.Group) {
if err = setUserSession(c, user); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to set signin session").SetInternal(err)
}
s.Collector.Collect(ctx, &metric.Metric{
Name: "user signed in",
})
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(user)); err != nil {
@@ -55,66 +50,41 @@ func (s *Server) registerAuthRoutes(g *echo.Group) {
})
g.POST("/auth/logout", func(c echo.Context) error {
ctx := c.Request().Context()
err := removeUserSession(c)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to set logout session").SetInternal(err)
}
s.Collector.Collect(ctx, &metric.Metric{
Name: "user logout",
})
c.Response().WriteHeader(http.StatusOK)
return nil
})
g.POST("/auth/signup", func(c echo.Context) error {
ctx := c.Request().Context()
// Don't allow to signup by this api if site host existed.
hostUserType := api.Host
hostUserFind := api.UserFind{
Role: &hostUserType,
}
hostUser, err := s.Store.FindUser(&hostUserFind)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find host user").SetInternal(err)
}
if hostUser != nil {
return echo.NewHTTPError(http.StatusUnauthorized, "Site Host existed, please contact the site host to signin account firstly.").SetInternal(err)
}
signup := &api.Signup{}
if err := json.NewDecoder(c.Request().Body).Decode(signup); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted signup request").SetInternal(err)
}
hostUserType := api.Host
hostUserFind := api.UserFind{
Role: &hostUserType,
// Validate signup form.
// We can do stricter checks later.
if len(signup.Email) < 6 {
return echo.NewHTTPError(http.StatusBadRequest, "Email is too short, minimum length is 6.")
}
hostUser, err := s.Store.FindUser(ctx, &hostUserFind)
if err != nil && common.ErrorCode(err) != common.NotFound {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find host user").SetInternal(err)
}
if signup.Role == api.Host && hostUser != nil {
return echo.NewHTTPError(http.StatusUnauthorized, "Site Host existed, please contact the site host to signin account firstly.").SetInternal(err)
}
systemSettingAllowSignUpName := api.SystemSettingAllowSignUpName
allowSignUpSetting, err := s.Store.FindSystemSetting(ctx, &api.SystemSettingFind{
Name: &systemSettingAllowSignUpName,
})
if err != nil && common.ErrorCode(err) != common.NotFound {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find system setting").SetInternal(err)
}
allowSignUpSettingValue := false
if allowSignUpSetting != nil {
err = json.Unmarshal([]byte(allowSignUpSetting.Value), &allowSignUpSettingValue)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to unmarshal system setting allow signup").SetInternal(err)
}
}
if !allowSignUpSettingValue && hostUser != nil {
return echo.NewHTTPError(http.StatusUnauthorized, "Site Host existed, please contact the site host to signin account firstly.").SetInternal(err)
}
userCreate := &api.UserCreate{
Username: signup.Username,
Role: api.Role(signup.Role),
Nickname: signup.Username,
Password: signup.Password,
OpenID: common.GenUUID(),
}
if err := userCreate.Validate(); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Invalid user create format.").SetInternal(err)
if len(signup.Password) < 6 {
return echo.NewHTTPError(http.StatusBadRequest, "Password is too short, minimum length is 6.")
}
passwordHash, err := bcrypt.GenerateFromPassword([]byte(signup.Password), bcrypt.DefaultCost)
@@ -122,15 +92,17 @@ func (s *Server) registerAuthRoutes(g *echo.Group) {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to generate password hash").SetInternal(err)
}
userCreate.PasswordHash = string(passwordHash)
user, err := s.Store.CreateUser(ctx, userCreate)
userCreate := &api.UserCreate{
Email: signup.Email,
Role: api.Role(signup.Role),
Name: signup.Name,
PasswordHash: string(passwordHash),
OpenID: common.GenUUID(),
}
user, err := s.Store.CreateUser(userCreate)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create user").SetInternal(err)
}
s.Collector.Collect(ctx, &metric.Metric{
Name: "user signed up",
})
err = setUserSession(c, user)
if err != nil {
+119
View File
@@ -0,0 +1,119 @@
package server
import (
"fmt"
"net/http"
"strconv"
"github.com/usememos/memos/api"
"github.com/usememos/memos/common"
"github.com/gorilla/sessions"
"github.com/labstack/echo-contrib/session"
"github.com/labstack/echo/v4"
)
var (
userIDContextKey = "user-id"
)
func getUserIDContextKey() string {
return userIDContextKey
}
func setUserSession(c echo.Context, user *api.User) error {
sess, _ := session.Get("session", c)
sess.Options = &sessions.Options{
Path: "/",
MaxAge: 1000 * 3600 * 24 * 30,
HttpOnly: true,
}
sess.Values[userIDContextKey] = user.ID
err := sess.Save(c.Request(), c.Response())
if err != nil {
return fmt.Errorf("failed to set session, err: %w", err)
}
return nil
}
func removeUserSession(c echo.Context) error {
sess, _ := session.Get("session", c)
sess.Options = &sessions.Options{
Path: "/",
MaxAge: 0,
HttpOnly: true,
}
sess.Values[userIDContextKey] = nil
err := sess.Save(c.Request(), c.Response())
if err != nil {
return fmt.Errorf("failed to set session, err: %w", err)
}
return nil
}
// Use session to store user.id.
func BasicAuthMiddleware(s *Server, next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
// Skip auth for some paths.
if common.HasPrefixes(c.Path(), "/api/auth", "/api/ping", "/api/status", "/api/user/:userId") {
return next(c)
}
// If there is openId in query string and related user is found, then skip auth.
openID := c.QueryParam("openId")
if openID != "" {
userFind := &api.UserFind{
OpenID: &openID,
}
user, err := s.Store.FindUser(userFind)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user by open_id").SetInternal(err)
}
if user != nil {
// Stores userID into context.
c.Set(getUserIDContextKey(), user.ID)
return next(c)
}
}
if common.HasPrefixes(c.Path(), "/api/memo", "/api/tag", "/api/shortcut") && c.Request().Method == http.MethodGet {
if _, err := strconv.Atoi(c.QueryParam("creatorId")); err == nil {
return next(c)
}
}
sess, err := session.Get("session", c)
if err != nil {
return echo.NewHTTPError(http.StatusUnauthorized, "Missing session").SetInternal(err)
}
userIDValue := sess.Values[userIDContextKey]
if userIDValue == nil {
return echo.NewHTTPError(http.StatusUnauthorized, "Missing userID in session")
}
userID, err := strconv.Atoi(fmt.Sprintf("%v", userIDValue))
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to malformatted user id in the session.").SetInternal(err)
}
// Even if there is no error, we still need to make sure the user still exists.
userFind := &api.UserFind{
ID: &userID,
}
user, err := s.Store.FindUser(userFind)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to find user by ID: %d", userID)).SetInternal(err)
}
if user == nil {
return echo.NewHTTPError(http.StatusUnauthorized, fmt.Sprintf("Not found user ID: %d", userID))
} else if user.RowStatus == api.Archived {
return echo.NewHTTPError(http.StatusForbidden, fmt.Sprintf("User has been archived with email %s", user.Email))
}
// Stores userID into context.
c.Set(getUserIDContextKey(), userID)
return next(c)
}
}
+3 -17
View File
@@ -12,8 +12,8 @@ import (
//go:embed dist
var embeddedFiles embed.FS
func getFileSystem(path string) http.FileSystem {
fs, err := fs.Sub(embeddedFiles, path)
func getFileSystem() http.FileSystem {
fs, err := fs.Sub(embeddedFiles, "dist")
if err != nil {
panic(err)
}
@@ -22,22 +22,8 @@ func getFileSystem(path string) http.FileSystem {
}
func embedFrontend(e *echo.Echo) {
// Use echo static middleware to serve the built dist folder
// refer: https://github.com/labstack/echo/blob/master/middleware/static.go
e.Use(middleware.StaticWithConfig(middleware.StaticConfig{
HTML5: true,
Filesystem: getFileSystem("dist"),
}))
g := e.Group("assets")
g.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
c.Response().Header().Set(echo.HeaderCacheControl, "max-age=31536000, immutable")
return next(c)
}
})
g.Use(middleware.StaticWithConfig(middleware.StaticConfig{
HTML5: true,
Filesystem: getFileSystem("dist/assets"),
Filesystem: getFileSystem(),
}))
}
-72
View File
@@ -1,72 +0,0 @@
package server
import (
"encoding/json"
"fmt"
"net/http"
"net/url"
"github.com/labstack/echo/v4"
getter "github.com/usememos/memos/plugin/http_getter"
metric "github.com/usememos/memos/plugin/metrics"
)
func (s *Server) registerGetterPublicRoutes(g *echo.Group) {
g.GET("/get/httpmeta", func(c echo.Context) error {
ctx := c.Request().Context()
urlStr := c.QueryParam("url")
if urlStr == "" {
return echo.NewHTTPError(http.StatusBadRequest, "Missing website url")
}
if _, err := url.Parse(urlStr); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Wrong url").SetInternal(err)
}
htmlMeta, err := getter.GetHTMLMeta(urlStr)
if err != nil {
return echo.NewHTTPError(http.StatusNotAcceptable, fmt.Sprintf("Failed to get website meta with url: %s", urlStr)).SetInternal(err)
}
s.Collector.Collect(ctx, &metric.Metric{
Name: "getter used",
Labels: map[string]string{
"type": "httpmeta",
},
})
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(htmlMeta)); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode website HTML meta").SetInternal(err)
}
return nil
})
g.GET("/get/image", func(c echo.Context) error {
ctx := c.Request().Context()
urlStr := c.QueryParam("url")
if urlStr == "" {
return echo.NewHTTPError(http.StatusBadRequest, "Missing image url")
}
if _, err := url.Parse(urlStr); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Wrong url").SetInternal(err)
}
image, err := getter.GetImage(urlStr)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Failed to get image url: %s", urlStr)).SetInternal(err)
}
s.Collector.Collect(ctx, &metric.Metric{
Name: "getter used",
Labels: map[string]string{
"type": "image",
},
})
c.Response().Writer.WriteHeader(http.StatusOK)
c.Response().Writer.Header().Set("Content-Type", image.Mediatype)
c.Response().Writer.Header().Set(echo.HeaderCacheControl, "max-age=31536000, immutable")
if _, err := c.Response().Writer.Write(image.Blob); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to write image blob").SetInternal(err)
}
return nil
})
}
+110 -434
View File
@@ -4,80 +4,33 @@ import (
"encoding/json"
"fmt"
"net/http"
"sort"
"strconv"
"strings"
"time"
"github.com/usememos/memos/api"
"github.com/usememos/memos/common"
metric "github.com/usememos/memos/plugin/metrics"
"github.com/labstack/echo/v4"
)
func (s *Server) registerMemoRoutes(g *echo.Group) {
g.POST("/memo", func(c echo.Context) error {
ctx := c.Request().Context()
userID, ok := c.Get(getUserIDContextKey()).(int)
if !ok {
return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
}
userID := c.Get(getUserIDContextKey()).(int)
memoCreate := &api.MemoCreate{
CreatorID: userID,
}
if err := json.NewDecoder(c.Request().Body).Decode(memoCreate); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post memo request").SetInternal(err)
}
if memoCreate.Content == "" {
return echo.NewHTTPError(http.StatusBadRequest, "Memo content shouldn't be empty")
if memoCreate.Visibility == nil || *memoCreate.Visibility == "" {
private := api.Privite
memoCreate.Visibility = &private
}
if memoCreate.Visibility == "" {
userSettingMemoVisibilityKey := api.UserSettingMemoVisibilityKey
userMemoVisibilitySetting, err := s.Store.FindUserSetting(ctx, &api.UserSettingFind{
UserID: userID,
Key: &userSettingMemoVisibilityKey,
})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user setting").SetInternal(err)
}
if userMemoVisibilitySetting != nil {
memoVisibility := api.Privite
err := json.Unmarshal([]byte(userMemoVisibilitySetting.Value), &memoVisibility)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to unmarshal user setting value").SetInternal(err)
}
memoCreate.Visibility = memoVisibility
} else {
// Private is the default memo visibility.
memoCreate.Visibility = api.Privite
}
}
memo, err := s.Store.CreateMemo(ctx, memoCreate)
memo, err := s.Store.CreateMemo(memoCreate)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create memo").SetInternal(err)
}
s.Collector.Collect(ctx, &metric.Metric{
Name: "memo created",
})
for _, resourceID := range memoCreate.ResourceIDList {
if _, err := s.Store.UpsertMemoResource(ctx, &api.MemoResourceUpsert{
MemoID: memo.ID,
ResourceID: resourceID,
}); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to upsert memo resource").SetInternal(err)
}
}
memo, err = s.Store.ComposeMemo(ctx, memo)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to compose memo").SetInternal(err)
}
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(memo)); err != nil {
@@ -87,53 +40,23 @@ func (s *Server) registerMemoRoutes(g *echo.Group) {
})
g.PATCH("/memo/:memoId", func(c echo.Context) error {
ctx := c.Request().Context()
userID, ok := c.Get(getUserIDContextKey()).(int)
if !ok {
return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
}
memoID, err := strconv.Atoi(c.Param("memoId"))
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("memoId"))).SetInternal(err)
}
memoFind := &api.MemoFind{
ID: &memoID,
CreatorID: &userID,
}
if _, err := s.Store.FindMemo(ctx, memoFind); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find memo").SetInternal(err)
}
currentTs := time.Now().Unix()
memoPatch := &api.MemoPatch{
ID: memoID,
UpdatedTs: &currentTs,
ID: memoID,
}
if err := json.NewDecoder(c.Request().Body).Decode(memoPatch); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted patch memo request").SetInternal(err)
}
memo, err := s.Store.PatchMemo(ctx, memoPatch)
memo, err := s.Store.PatchMemo(memoPatch)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to patch memo").SetInternal(err)
}
for _, resourceID := range memoPatch.ResourceIDList {
if _, err := s.Store.UpsertMemoResource(ctx, &api.MemoResourceUpsert{
MemoID: memo.ID,
ResourceID: resourceID,
}); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to upsert memo resource").SetInternal(err)
}
}
memo, err = s.Store.ComposeMemo(ctx, memo)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to compose memo").SetInternal(err)
}
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(memo)); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode memo response").SetInternal(err)
@@ -142,24 +65,24 @@ func (s *Server) registerMemoRoutes(g *echo.Group) {
})
g.GET("/memo", func(c echo.Context) error {
ctx := c.Request().Context()
memoFind := &api.MemoFind{}
if userID, err := strconv.Atoi(c.QueryParam("creatorId")); err == nil {
memoFind.CreatorID = &userID
} else {
userID, ok := c.Get(getUserIDContextKey()).(int)
if !ok {
return echo.NewHTTPError(http.StatusBadRequest, "Missing user id to find memo")
}
memoFind.CreatorID = &userID
}
currentUserID, ok := c.Get(getUserIDContextKey()).(int)
// Only can get PUBLIC memos in visitor mode
_, ok := c.Get(getUserIDContextKey()).(int)
if !ok {
if memoFind.CreatorID == nil {
return echo.NewHTTPError(http.StatusBadRequest, "Missing user id to find memo")
}
memoFind.VisibilityList = []api.Visibility{api.Public}
} else {
if memoFind.CreatorID == nil {
memoFind.CreatorID = &currentUserID
} else {
memoFind.VisibilityList = []api.Visibility{api.Public, api.Protected}
}
publicVisibility := api.Public
memoFind.Visibility = &publicVisibility
}
rowStatus := api.RowStatus(c.QueryParam("rowStatus"))
@@ -176,14 +99,6 @@ func (s *Server) registerMemoRoutes(g *echo.Group) {
contentSearch := "#" + tag + " "
memoFind.ContentSearch = &contentSearch
}
visibilitListStr := c.QueryParam("visibility")
if visibilitListStr != "" {
visibilityList := []api.Visibility{}
for _, visibility := range strings.Split(visibilitListStr, ",") {
visibilityList = append(visibilityList, api.Visibility(visibility))
}
memoFind.VisibilityList = visibilityList
}
if limit, err := strconv.Atoi(c.QueryParam("limit")); err == nil {
memoFind.Limit = limit
}
@@ -191,55 +106,108 @@ func (s *Server) registerMemoRoutes(g *echo.Group) {
memoFind.Offset = offset
}
list, err := s.Store.FindMemoList(ctx, memoFind)
list, err := s.Store.FindMemoList(memoFind)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch memo list").SetInternal(err)
}
var pinnedMemoList []*api.Memo
var unpinnedMemoList []*api.Memo
for _, memo := range list {
if memo.Pinned {
pinnedMemoList = append(pinnedMemoList, memo)
} else {
unpinnedMemoList = append(unpinnedMemoList, memo)
}
}
sort.Slice(pinnedMemoList, func(i, j int) bool {
return pinnedMemoList[i].DisplayTs > pinnedMemoList[j].DisplayTs
})
sort.Slice(unpinnedMemoList, func(i, j int) bool {
return unpinnedMemoList[i].DisplayTs > unpinnedMemoList[j].DisplayTs
})
memoList := []*api.Memo{}
memoList = append(memoList, pinnedMemoList...)
memoList = append(memoList, unpinnedMemoList...)
if memoFind.Limit != 0 {
memoList = memoList[memoFind.Offset:common.Min(len(memoList), memoFind.Offset+memoFind.Limit)]
}
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(memoList)); err != nil {
if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(list)); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode memo list response").SetInternal(err)
}
return nil
})
g.GET("/memo/amount", func(c echo.Context) error {
ctx := c.Request().Context()
normalRowStatus := api.Normal
memoFind := &api.MemoFind{
RowStatus: &normalRowStatus,
}
if userID, err := strconv.Atoi(c.QueryParam("userId")); err == nil {
memoFind.CreatorID = &userID
g.POST("/memo/:memoId/organizer", func(c echo.Context) error {
memoID, err := strconv.Atoi(c.Param("memoId"))
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("memoId"))).SetInternal(err)
}
memoList, err := s.Store.FindMemoList(ctx, memoFind)
userID := c.Get(getUserIDContextKey()).(int)
memoOrganizerUpsert := &api.MemoOrganizerUpsert{
MemoID: memoID,
UserID: userID,
}
if err := json.NewDecoder(c.Request().Body).Decode(memoOrganizerUpsert); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post memo organizer request").SetInternal(err)
}
err = s.Store.UpsertMemoOrganizer(memoOrganizerUpsert)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to upsert memo organizer").SetInternal(err)
}
memo, err := s.Store.FindMemo(&api.MemoFind{
ID: &memoID,
})
if err != nil {
if common.ErrorCode(err) == common.NotFound {
return echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("Memo ID not found: %d", memoID)).SetInternal(err)
}
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to find memo by ID: %v", memoID)).SetInternal(err)
}
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(memo)); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode memo response").SetInternal(err)
}
return nil
})
g.GET("/memo/:memoId", func(c echo.Context) error {
memoID, err := strconv.Atoi(c.Param("memoId"))
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("memoId"))).SetInternal(err)
}
memoFind := &api.MemoFind{
ID: &memoID,
}
memo, err := s.Store.FindMemo(memoFind)
if err != nil {
if common.ErrorCode(err) == common.NotFound {
return echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("Memo ID not found: %d", memoID)).SetInternal(err)
}
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to find memo by ID: %v", memoID)).SetInternal(err)
}
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(memo)); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode memo response").SetInternal(err)
}
return nil
})
g.DELETE("/memo/:memoId", func(c echo.Context) error {
memoID, err := strconv.Atoi(c.Param("memoId"))
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("memoId"))).SetInternal(err)
}
memoDelete := &api.MemoDelete{
ID: memoID,
}
err = s.Store.DeleteMemo(memoDelete)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to delete memo ID: %v", memoID)).SetInternal(err)
}
return c.JSON(http.StatusOK, true)
})
g.GET("/memo/amount", func(c echo.Context) error {
userID := c.Get(getUserIDContextKey()).(int)
normalRowStatus := api.Normal
memoFind := &api.MemoFind{
CreatorID: &userID,
RowStatus: &normalRowStatus,
}
memoList, err := s.Store.FindMemoList(memoFind)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find memo list").SetInternal(err)
}
@@ -250,296 +218,4 @@ func (s *Server) registerMemoRoutes(g *echo.Group) {
}
return nil
})
g.GET("/memo/stats", func(c echo.Context) error {
ctx := c.Request().Context()
normalStatus := api.Normal
memoFind := &api.MemoFind{
RowStatus: &normalStatus,
}
if creatorID, err := strconv.Atoi(c.QueryParam("creatorId")); err == nil {
memoFind.CreatorID = &creatorID
}
if memoFind.CreatorID == nil {
return echo.NewHTTPError(http.StatusBadRequest, "Missing user id to find memo")
}
currentUserID, ok := c.Get(getUserIDContextKey()).(int)
if !ok {
memoFind.VisibilityList = []api.Visibility{api.Public}
} else {
if *memoFind.CreatorID != currentUserID {
memoFind.VisibilityList = []api.Visibility{api.Public, api.Protected}
} else {
memoFind.VisibilityList = []api.Visibility{api.Public, api.Protected, api.Privite}
}
}
list, err := s.Store.FindMemoList(ctx, memoFind)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch memo list").SetInternal(err)
}
displayTsList := []int64{}
for _, memo := range list {
displayTsList = append(displayTsList, memo.DisplayTs)
}
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(displayTsList)); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode memo stats response").SetInternal(err)
}
return nil
})
g.GET("/memo/all", func(c echo.Context) error {
ctx := c.Request().Context()
memoFind := &api.MemoFind{}
_, ok := c.Get(getUserIDContextKey()).(int)
if !ok {
memoFind.VisibilityList = []api.Visibility{api.Public}
} else {
memoFind.VisibilityList = []api.Visibility{api.Public, api.Protected}
}
pinnedStr := c.QueryParam("pinned")
if pinnedStr != "" {
pinned := pinnedStr == "true"
memoFind.Pinned = &pinned
}
tag := c.QueryParam("tag")
if tag != "" {
contentSearch := "#" + tag + " "
memoFind.ContentSearch = &contentSearch
}
visibilitListStr := c.QueryParam("visibility")
if visibilitListStr != "" {
visibilityList := []api.Visibility{}
for _, visibility := range strings.Split(visibilitListStr, ",") {
visibilityList = append(visibilityList, api.Visibility(visibility))
}
memoFind.VisibilityList = visibilityList
}
if limit, err := strconv.Atoi(c.QueryParam("limit")); err == nil {
memoFind.Limit = limit
}
if offset, err := strconv.Atoi(c.QueryParam("offset")); err == nil {
memoFind.Offset = offset
}
// Only fetch normal status memos.
normalStatus := api.Normal
memoFind.RowStatus = &normalStatus
list, err := s.Store.FindMemoList(ctx, memoFind)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch all memo list").SetInternal(err)
}
sort.Slice(list, func(i, j int) bool {
return list[i].DisplayTs > list[j].DisplayTs
})
if memoFind.Limit != 0 {
list = list[memoFind.Offset:common.Min(len(list), memoFind.Offset+memoFind.Limit)]
}
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(list)); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode all memo list response").SetInternal(err)
}
return nil
})
g.GET("/memo/:memoId", func(c echo.Context) error {
ctx := c.Request().Context()
memoID, err := strconv.Atoi(c.Param("memoId"))
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("memoId"))).SetInternal(err)
}
memoFind := &api.MemoFind{
ID: &memoID,
}
memo, err := s.Store.FindMemo(ctx, memoFind)
if err != nil {
if common.ErrorCode(err) == common.NotFound {
return echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("Memo ID not found: %d", memoID)).SetInternal(err)
}
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to find memo by ID: %v", memoID)).SetInternal(err)
}
userID, ok := c.Get(getUserIDContextKey()).(int)
if memo.Visibility == api.Privite {
if !ok || memo.CreatorID != userID {
return echo.NewHTTPError(http.StatusForbidden, "this memo is private only")
}
} else if memo.Visibility == api.Protected {
if !ok {
return echo.NewHTTPError(http.StatusForbidden, "this memo is protected, missing user in session")
}
}
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(memo)); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode memo response").SetInternal(err)
}
return nil
})
g.POST("/memo/:memoId/organizer", func(c echo.Context) error {
ctx := c.Request().Context()
memoID, err := strconv.Atoi(c.Param("memoId"))
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("memoId"))).SetInternal(err)
}
userID, ok := c.Get(getUserIDContextKey()).(int)
if !ok {
return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
}
memoOrganizerUpsert := &api.MemoOrganizerUpsert{
MemoID: memoID,
UserID: userID,
}
if err := json.NewDecoder(c.Request().Body).Decode(memoOrganizerUpsert); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post memo organizer request").SetInternal(err)
}
err = s.Store.UpsertMemoOrganizer(ctx, memoOrganizerUpsert)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to upsert memo organizer").SetInternal(err)
}
memo, err := s.Store.FindMemo(ctx, &api.MemoFind{
ID: &memoID,
})
if err != nil {
if common.ErrorCode(err) == common.NotFound {
return echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("Memo ID not found: %d", memoID)).SetInternal(err)
}
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to find memo by ID: %v", memoID)).SetInternal(err)
}
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(memo)); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode memo response").SetInternal(err)
}
return nil
})
g.POST("/memo/:memoId/resource", func(c echo.Context) error {
ctx := c.Request().Context()
memoID, err := strconv.Atoi(c.Param("memoId"))
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("memoId"))).SetInternal(err)
}
currentTs := time.Now().Unix()
memoResourceUpsert := &api.MemoResourceUpsert{
MemoID: memoID,
UpdatedTs: &currentTs,
}
if err := json.NewDecoder(c.Request().Body).Decode(memoResourceUpsert); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post memo resource request").SetInternal(err)
}
if _, err := s.Store.UpsertMemoResource(ctx, memoResourceUpsert); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to upsert memo resource").SetInternal(err)
}
resourceFind := &api.ResourceFind{
ID: &memoResourceUpsert.ResourceID,
}
resource, err := s.Store.FindResource(ctx, resourceFind)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch resource").SetInternal(err)
}
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(resource)); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode resource response").SetInternal(err)
}
return nil
})
g.GET("/memo/:memoId/resource", func(c echo.Context) error {
ctx := c.Request().Context()
memoID, err := strconv.Atoi(c.Param("memoId"))
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("memoId"))).SetInternal(err)
}
resourceFind := &api.ResourceFind{
MemoID: &memoID,
}
resourceList, err := s.Store.FindResourceList(ctx, resourceFind)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch resource list").SetInternal(err)
}
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(resourceList)); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode resource list response").SetInternal(err)
}
return nil
})
g.DELETE("/memo/:memoId/resource/:resourceId", func(c echo.Context) error {
ctx := c.Request().Context()
memoID, err := strconv.Atoi(c.Param("memoId"))
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Memo ID is not a number: %s", c.Param("memoId"))).SetInternal(err)
}
resourceID, err := strconv.Atoi(c.Param("resourceId"))
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Resource ID is not a number: %s", c.Param("resourceId"))).SetInternal(err)
}
memoResourceDelete := &api.MemoResourceDelete{
MemoID: &memoID,
ResourceID: &resourceID,
}
if err := s.Store.DeleteMemoResource(ctx, memoResourceDelete); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch resource list").SetInternal(err)
}
return c.JSON(http.StatusOK, true)
})
g.DELETE("/memo/:memoId", func(c echo.Context) error {
ctx := c.Request().Context()
userID, ok := c.Get(getUserIDContextKey()).(int)
if !ok {
return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
}
memoID, err := strconv.Atoi(c.Param("memoId"))
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("memoId"))).SetInternal(err)
}
memoFind := &api.MemoFind{
ID: &memoID,
CreatorID: &userID,
}
if _, err := s.Store.FindMemo(ctx, memoFind); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find memo").SetInternal(err)
}
memoDelete := &api.MemoDelete{
ID: memoID,
}
if err := s.Store.DeleteMemo(ctx, memoDelete); err != nil {
if common.ErrorCode(err) == common.NotFound {
return echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("Memo ID not found: %d", memoID))
}
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to delete memo ID: %v", memoID)).SetInternal(err)
}
return c.JSON(http.StatusOK, true)
})
}
-49
View File
@@ -1,49 +0,0 @@
package server
import (
"context"
"fmt"
metric "github.com/usememos/memos/plugin/metrics"
"github.com/usememos/memos/plugin/metrics/segment"
"github.com/usememos/memos/server/profile"
"github.com/usememos/memos/server/version"
"github.com/usememos/memos/store"
)
// MetricCollector is the metric collector.
type MetricCollector struct {
collector metric.Collector
profile *profile.Profile
store *store.Store
}
const (
segmentMetricWriteKey = "fTn5BumOkj352n3TGw9tu0ARH2dOkcoQ"
)
func NewMetricCollector(profile *profile.Profile, store *store.Store) MetricCollector {
c := segment.NewCollector(segmentMetricWriteKey)
return MetricCollector{
collector: c,
profile: profile,
store: store,
}
}
func (mc *MetricCollector) Collect(_ context.Context, metric *metric.Metric) {
if mc.profile.Mode == "dev" {
return
}
if metric.Labels == nil {
metric.Labels = map[string]string{}
}
metric.Labels["version"] = version.GetCurrentVersion(mc.profile.Mode)
err := mc.collector.Collect(metric)
if err != nil {
fmt.Printf("Failed to request segment, error: %+v\n", err)
}
}
+5 -5
View File
@@ -7,7 +7,7 @@ import (
"path/filepath"
"strings"
"github.com/usememos/memos/server/version"
"github.com/usememos/memos/common"
)
// Profile is the configuration to start main server.
@@ -45,7 +45,7 @@ func checkDSN(dataDir string) (string, error) {
}
// GetDevProfile will return a profile for dev or prod.
func GetProfile() (*Profile, error) {
func GetProfile() *Profile {
profile := Profile{}
flag.StringVar(&profile.Mode, "mode", "dev", "mode of server")
flag.IntVar(&profile.Port, "port", 8080, "port of server")
@@ -63,12 +63,12 @@ func GetProfile() (*Profile, error) {
dataDir, err := checkDSN(profile.Data)
if err != nil {
fmt.Printf("Failed to check dsn: %s, err: %+v\n", dataDir, err)
return nil, err
os.Exit(1)
}
profile.Data = dataDir
profile.DSN = fmt.Sprintf("%s/memos_%s.db", dataDir, profile.Mode)
profile.Version = version.GetCurrentVersion(profile.Mode)
profile.Version = common.GetCurrentVersion(profile.Mode)
return &profile, nil
return &profile
}
+14 -142
View File
@@ -3,41 +3,26 @@ package server
import (
"encoding/json"
"fmt"
"html"
"io"
"io/ioutil"
"net/http"
"strconv"
"time"
"github.com/usememos/memos/api"
"github.com/usememos/memos/common"
metric "github.com/usememos/memos/plugin/metrics"
"github.com/labstack/echo/v4"
)
const (
// The max file size is 32MB.
maxFileSize = (32 * 8) << 20
)
func (s *Server) registerResourceRoutes(g *echo.Group) {
g.POST("/resource", func(c echo.Context) error {
ctx := c.Request().Context()
userID, ok := c.Get(getUserIDContextKey()).(int)
if !ok {
return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
}
userID := c.Get(getUserIDContextKey()).(int)
if err := c.Request().ParseMultipartForm(maxFileSize); err != nil {
err := c.Request().ParseMultipartForm(64 << 20)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Upload file overload max size").SetInternal(err)
}
file, err := c.FormFile("file")
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to get uploading file").SetInternal(err)
}
if file == nil {
return echo.NewHTTPError(http.StatusBadRequest, "Upload file not found").SetInternal(err)
}
@@ -50,7 +35,7 @@ func (s *Server) registerResourceRoutes(g *echo.Group) {
}
defer src.Close()
fileBytes, err := io.ReadAll(src)
fileBytes, err := ioutil.ReadAll(src)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to read file").SetInternal(err)
}
@@ -63,13 +48,10 @@ func (s *Server) registerResourceRoutes(g *echo.Group) {
CreatorID: userID,
}
resource, err := s.Store.CreateResource(ctx, resourceCreate)
resource, err := s.Store.CreateResource(resourceCreate)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create resource").SetInternal(err)
}
s.Collector.Collect(ctx, &metric.Metric{
Name: "resource created",
})
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(resource)); err != nil {
@@ -79,29 +61,15 @@ func (s *Server) registerResourceRoutes(g *echo.Group) {
})
g.GET("/resource", func(c echo.Context) error {
ctx := c.Request().Context()
userID, ok := c.Get(getUserIDContextKey()).(int)
if !ok {
return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
}
userID := c.Get(getUserIDContextKey()).(int)
resourceFind := &api.ResourceFind{
CreatorID: &userID,
}
list, err := s.Store.FindResourceList(ctx, resourceFind)
list, err := s.Store.FindResourceList(resourceFind)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch resource list").SetInternal(err)
}
for _, resource := range list {
memoResoureceList, err := s.Store.FindMemoResourceList(ctx, &api.MemoResourceFind{
ResourceID: &resource.ID,
})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find memo resource list").SetInternal(err)
}
resource.LinkedMemoAmount = len(memoResoureceList)
}
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(list)); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode resource list response").SetInternal(err)
@@ -110,21 +78,17 @@ func (s *Server) registerResourceRoutes(g *echo.Group) {
})
g.GET("/resource/:resourceId", func(c echo.Context) error {
ctx := c.Request().Context()
resourceID, err := strconv.Atoi(c.Param("resourceId"))
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("resourceId"))).SetInternal(err)
}
userID, ok := c.Get(getUserIDContextKey()).(int)
if !ok {
return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
}
userID := c.Get(getUserIDContextKey()).(int)
resourceFind := &api.ResourceFind{
ID: &resourceID,
CreatorID: &userID,
}
resource, err := s.Store.FindResource(ctx, resourceFind)
resource, err := s.Store.FindResource(resourceFind)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch resource").SetInternal(err)
}
@@ -137,21 +101,17 @@ func (s *Server) registerResourceRoutes(g *echo.Group) {
})
g.GET("/resource/:resourceId/blob", func(c echo.Context) error {
ctx := c.Request().Context()
resourceID, err := strconv.Atoi(c.Param("resourceId"))
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("resourceId"))).SetInternal(err)
}
userID, ok := c.Get(getUserIDContextKey()).(int)
if !ok {
return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
}
userID := c.Get(getUserIDContextKey()).(int)
resourceFind := &api.ResourceFind{
ID: &resourceID,
CreatorID: &userID,
}
resource, err := s.Store.FindResource(ctx, resourceFind)
resource, err := s.Store.FindResource(resourceFind)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch resource").SetInternal(err)
}
@@ -161,111 +121,23 @@ func (s *Server) registerResourceRoutes(g *echo.Group) {
if _, err := c.Response().Writer.Write(resource.Blob); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to write resource blob").SetInternal(err)
}
return nil
})
g.DELETE("/resource/:resourceId", func(c echo.Context) error {
ctx := c.Request().Context()
userID, ok := c.Get(getUserIDContextKey()).(int)
if !ok {
return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
}
resourceID, err := strconv.Atoi(c.Param("resourceId"))
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("resourceId"))).SetInternal(err)
}
resource, err := s.Store.FindResource(ctx, &api.ResourceFind{
ID: &resourceID,
CreatorID: &userID,
})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find resource").SetInternal(err)
}
if resource == nil {
return echo.NewHTTPError(http.StatusNotFound, "Not find resource").SetInternal(err)
}
resourceDelete := &api.ResourceDelete{
ID: resourceID,
}
if err := s.Store.DeleteResource(ctx, resourceDelete); err != nil {
if common.ErrorCode(err) == common.NotFound {
return echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("Resource ID not found: %d", resourceID))
}
if err := s.Store.DeleteResource(resourceDelete); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to delete resource").SetInternal(err)
}
return c.JSON(http.StatusOK, true)
})
g.PATCH("/resource/:resourceId", func(c echo.Context) error {
ctx := c.Request().Context()
userID, ok := c.Get(getUserIDContextKey()).(int)
if !ok {
return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
}
resourceID, err := strconv.Atoi(c.Param("resourceId"))
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("resourceId"))).SetInternal(err)
}
resourceFind := &api.ResourceFind{
ID: &resourceID,
CreatorID: &userID,
}
if _, err := s.Store.FindResource(ctx, resourceFind); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find resource").SetInternal(err)
}
currentTs := time.Now().Unix()
resourcePatch := &api.ResourcePatch{
ID: resourceID,
UpdatedTs: &currentTs,
}
if err := json.NewDecoder(c.Request().Body).Decode(resourcePatch); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted patch resource request").SetInternal(err)
}
resource, err := s.Store.PatchResource(ctx, resourcePatch)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to patch resource").SetInternal(err)
}
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(resource)); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode resource response").SetInternal(err)
}
return nil
})
}
func (s *Server) registerResourcePublicRoutes(g *echo.Group) {
g.GET("/r/:resourceId/:filename", func(c echo.Context) error {
ctx := c.Request().Context()
resourceID, err := strconv.Atoi(c.Param("resourceId"))
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("resourceId"))).SetInternal(err)
}
filename := html.UnescapeString(c.Param("filename"))
resourceFind := &api.ResourceFind{
ID: &resourceID,
Filename: &filename,
}
resource, err := s.Store.FindResource(ctx, resourceFind)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to fetch resource ID: %v", resourceID)).SetInternal(err)
}
c.Response().Writer.WriteHeader(http.StatusOK)
c.Response().Writer.Header().Set("Content-Type", resource.Type)
c.Response().Writer.Header().Set(echo.HeaderCacheControl, "max-age=31536000, immutable")
if _, err := c.Response().Writer.Write(resource.Blob); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to write response").SetInternal(err)
}
return nil
})
}
-72
View File
@@ -1,72 +0,0 @@
package server
import (
"net/http"
"strconv"
"time"
"github.com/gorilla/feeds"
"github.com/labstack/echo/v4"
"github.com/usememos/memos/api"
)
func (s *Server) registerRSSRoutes(g *echo.Group) {
g.GET("/u/:id/rss.xml", func(c echo.Context) error {
ctx := c.Request().Context()
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "User id is not a number").SetInternal(err)
}
normalStatus := api.Normal
memoFind := api.MemoFind{
CreatorID: &id,
RowStatus: &normalStatus,
VisibilityList: []api.Visibility{
api.Public,
},
}
memoList, err := s.Store.FindMemoList(ctx, &memoFind)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find memo list").SetInternal(err)
}
userFind := api.UserFind{
ID: &id,
}
user, err := s.Store.FindUser(ctx, &userFind)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user").SetInternal(err)
}
baseURL := c.Scheme() + "://" + c.Request().Host
feed := &feeds.Feed{
Title: "Memos",
Link: &feeds.Link{Href: baseURL},
Description: "Memos",
Author: &feeds.Author{Name: user.Username},
Created: time.Now(),
}
feed.Items = make([]*feeds.Item, len(memoList))
for i, memo := range memoList {
feed.Items[i] = &feeds.Item{
Title: user.Username + "-memos-" + strconv.Itoa(memo.ID),
Link: &feeds.Link{Href: baseURL + "/m/" + strconv.Itoa(memo.ID)},
Description: memo.Content,
Created: time.Unix(memo.CreatedTs, 0),
}
}
rss, err := feed.ToRss()
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to generate rss").SetInternal(err)
}
rssPrefix := `<?xml version="1.0" encoding="UTF-8"?>`
return c.XMLBlob(http.StatusOK, []byte(rss[len(rssPrefix):]))
})
}
+4 -16
View File
@@ -17,8 +17,6 @@ import (
type Server struct {
e *echo.Echo
Collector *MetricCollector
Profile *profile.Profile
Store *store.Store
@@ -31,13 +29,9 @@ func NewServer(profile *profile.Profile) *Server {
e.HidePort = true
e.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{
Format: `{"time":"${time_rfc3339}",` +
`"method":"${method}","uri":"${uri}",` +
`"status":${status},"error":"${error}"}` + "\n",
Format: "${method} ${uri} ${status}\n",
}))
e.Use(middleware.CORS())
e.Use(middleware.TimeoutWithConfig(middleware.TimeoutConfig{
Skipper: middleware.DefaultSkipper,
ErrorMessage: "Request timeout",
@@ -58,19 +52,13 @@ func NewServer(profile *profile.Profile) *Server {
Profile: profile,
}
rootGroup := e.Group("")
s.registerRSSRoutes(rootGroup)
// Webhooks api skips auth checker.
webhookGroup := e.Group("/h")
s.registerResourcePublicRoutes(webhookGroup)
publicGroup := e.Group("/o")
s.registerResourcePublicRoutes(publicGroup)
s.registerGetterPublicRoutes(publicGroup)
s.registerWebhookRoutes(webhookGroup)
apiGroup := e.Group("/api")
apiGroup.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
return aclMiddleware(s, next)
return BasicAuthMiddleware(s, next)
})
s.registerSystemRoutes(apiGroup)
s.registerAuthRoutes(apiGroup)
+8 -27
View File
@@ -5,22 +5,15 @@ import (
"fmt"
"net/http"
"strconv"
"time"
"github.com/usememos/memos/api"
"github.com/usememos/memos/common"
metric "github.com/usememos/memos/plugin/metrics"
"github.com/labstack/echo/v4"
)
func (s *Server) registerShortcutRoutes(g *echo.Group) {
g.POST("/shortcut", func(c echo.Context) error {
ctx := c.Request().Context()
userID, ok := c.Get(getUserIDContextKey()).(int)
if !ok {
return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
}
userID := c.Get(getUserIDContextKey()).(int)
shortcutCreate := &api.ShortcutCreate{
CreatorID: userID,
}
@@ -28,13 +21,10 @@ func (s *Server) registerShortcutRoutes(g *echo.Group) {
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post shortcut request").SetInternal(err)
}
shortcut, err := s.Store.CreateShortcut(ctx, shortcutCreate)
shortcut, err := s.Store.CreateShortcut(shortcutCreate)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create shortcut").SetInternal(err)
}
s.Collector.Collect(ctx, &metric.Metric{
Name: "shortcut created",
})
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(shortcut)); err != nil {
@@ -44,22 +34,19 @@ func (s *Server) registerShortcutRoutes(g *echo.Group) {
})
g.PATCH("/shortcut/:shortcutId", func(c echo.Context) error {
ctx := c.Request().Context()
shortcutID, err := strconv.Atoi(c.Param("shortcutId"))
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("shortcutId"))).SetInternal(err)
}
currentTs := time.Now().Unix()
shortcutPatch := &api.ShortcutPatch{
ID: shortcutID,
UpdatedTs: &currentTs,
ID: shortcutID,
}
if err := json.NewDecoder(c.Request().Body).Decode(shortcutPatch); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted patch shortcut request").SetInternal(err)
}
shortcut, err := s.Store.PatchShortcut(ctx, shortcutPatch)
shortcut, err := s.Store.PatchShortcut(shortcutPatch)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to patch shortcut").SetInternal(err)
}
@@ -72,7 +59,6 @@ func (s *Server) registerShortcutRoutes(g *echo.Group) {
})
g.GET("/shortcut", func(c echo.Context) error {
ctx := c.Request().Context()
shortcutFind := &api.ShortcutFind{}
if userID, err := strconv.Atoi(c.QueryParam("creatorId")); err == nil {
@@ -86,7 +72,7 @@ func (s *Server) registerShortcutRoutes(g *echo.Group) {
shortcutFind.CreatorID = &userID
}
list, err := s.Store.FindShortcutList(ctx, shortcutFind)
list, err := s.Store.FindShortcutList(shortcutFind)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch shortcut list").SetInternal(err)
}
@@ -99,7 +85,6 @@ func (s *Server) registerShortcutRoutes(g *echo.Group) {
})
g.GET("/shortcut/:shortcutId", func(c echo.Context) error {
ctx := c.Request().Context()
shortcutID, err := strconv.Atoi(c.Param("shortcutId"))
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("shortcutId"))).SetInternal(err)
@@ -108,7 +93,7 @@ func (s *Server) registerShortcutRoutes(g *echo.Group) {
shortcutFind := &api.ShortcutFind{
ID: &shortcutID,
}
shortcut, err := s.Store.FindShortcut(ctx, shortcutFind)
shortcut, err := s.Store.FindShortcut(shortcutFind)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to fetch shortcut by ID %d", *shortcutFind.ID)).SetInternal(err)
}
@@ -121,19 +106,15 @@ func (s *Server) registerShortcutRoutes(g *echo.Group) {
})
g.DELETE("/shortcut/:shortcutId", func(c echo.Context) error {
ctx := c.Request().Context()
shortcutID, err := strconv.Atoi(c.Param("shortcutId"))
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("shortcutId"))).SetInternal(err)
}
shortcutDelete := &api.ShortcutDelete{
ID: &shortcutID,
ID: shortcutID,
}
if err := s.Store.DeleteShortcut(ctx, shortcutDelete); err != nil {
if common.ErrorCode(err) == common.NotFound {
return echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("Shortcut ID not found: %d", shortcutID))
}
if err := s.Store.DeleteShortcut(shortcutDelete); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to delete shortcut").SetInternal(err)
}
+4 -106
View File
@@ -3,11 +3,8 @@ package server
import (
"encoding/json"
"net/http"
"os"
"github.com/usememos/memos/api"
"github.com/usememos/memos/common"
metric "github.com/usememos/memos/plugin/metrics"
"github.com/labstack/echo/v4"
)
@@ -24,13 +21,12 @@ func (s *Server) registerSystemRoutes(g *echo.Group) {
})
g.GET("/status", func(c echo.Context) error {
ctx := c.Request().Context()
hostUserType := api.Host
hostUserFind := api.UserFind{
Role: &hostUserType,
}
hostUser, err := s.Store.FindUser(ctx, &hostUserFind)
if err != nil && common.ErrorCode(err) != common.NotFound {
hostUser, err := s.Store.FindUser(&hostUserFind)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find host user").SetInternal(err)
}
@@ -40,49 +36,8 @@ func (s *Server) registerSystemRoutes(g *echo.Group) {
}
systemStatus := api.SystemStatus{
Host: hostUser,
Profile: *s.Profile,
DBSize: 0,
AllowSignUp: false,
AdditionalStyle: "",
AdditionalScript: "",
}
systemSettingList, err := s.Store.FindSystemSettingList(ctx, &api.SystemSettingFind{})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find system setting list").SetInternal(err)
}
for _, systemSetting := range systemSettingList {
var value interface{}
err = json.Unmarshal([]byte(systemSetting.Value), &value)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to unmarshal system setting").SetInternal(err)
}
if systemSetting.Name == api.SystemSettingAllowSignUpName {
systemStatus.AllowSignUp = value.(bool)
} else if systemSetting.Name == api.SystemSettingAdditionalStyleName {
systemStatus.AdditionalStyle = value.(string)
} else if systemSetting.Name == api.SystemSettingAdditionalScriptName {
systemStatus.AdditionalScript = value.(string)
}
}
userID, ok := c.Get(getUserIDContextKey()).(int)
if ok {
user, err := s.Store.FindUser(ctx, &api.UserFind{
ID: &userID,
})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user").SetInternal(err)
}
if user != nil && user.Role == api.Host {
fi, err := os.Stat(s.Profile.DSN)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to read database fileinfo").SetInternal(err)
}
systemStatus.DBSize = fi.Size()
}
Host: hostUser,
Profile: s.Profile,
}
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
@@ -91,61 +46,4 @@ func (s *Server) registerSystemRoutes(g *echo.Group) {
}
return nil
})
g.POST("/system/setting", func(c echo.Context) error {
ctx := c.Request().Context()
userID, ok := c.Get(getUserIDContextKey()).(int)
if !ok {
return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
}
user, err := s.Store.FindUser(ctx, &api.UserFind{
ID: &userID,
})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user").SetInternal(err)
}
if user == nil {
return echo.NewHTTPError(http.StatusNotFound, "Current signin user not found")
} else if user.Role != api.Host {
return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized")
}
systemSettingUpsert := &api.SystemSettingUpsert{}
if err := json.NewDecoder(c.Request().Body).Decode(systemSettingUpsert); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post system setting request").SetInternal(err)
}
if err := systemSettingUpsert.Validate(); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "system setting invalidate").SetInternal(err)
}
systemSetting, err := s.Store.UpsertSystemSetting(ctx, systemSettingUpsert)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to upsert system setting").SetInternal(err)
}
s.Collector.Collect(ctx, &metric.Metric{
Name: "systemSetting updated",
Labels: map[string]string{"field": string(systemSettingUpsert.Name)},
})
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(systemSetting)); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode system setting response").SetInternal(err)
}
return nil
})
g.GET("/system/setting", func(c echo.Context) error {
ctx := c.Request().Context()
systemSettingList, err := s.Store.FindSystemSettingList(ctx, &api.SystemSettingFind{})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find system setting list").SetInternal(err)
}
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(systemSettingList)); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode system setting list response").SetInternal(err)
}
return nil
})
}
+22 -34
View File
@@ -12,11 +12,8 @@ import (
"github.com/labstack/echo/v4"
)
var tagRegexpList = []*regexp.Regexp{regexp.MustCompile(`^#([^\s#]+?) `), regexp.MustCompile(`[^\S]#([^\s#]+?) `), regexp.MustCompile(` #([^\s#]+?) `)}
func (s *Server) registerTagRoutes(g *echo.Group) {
g.GET("/tag", func(c echo.Context) error {
ctx := c.Request().Context()
contentSearch := "#"
normalRowStatus := api.Normal
memoFind := api.MemoFind{
@@ -26,37 +23,45 @@ func (s *Server) registerTagRoutes(g *echo.Group) {
if userID, err := strconv.Atoi(c.QueryParam("creatorId")); err == nil {
memoFind.CreatorID = &userID
}
currentUserID, ok := c.Get(getUserIDContextKey()).(int)
if !ok {
if memoFind.CreatorID == nil {
return echo.NewHTTPError(http.StatusBadRequest, "Missing user id to find memo")
}
memoFind.VisibilityList = []api.Visibility{api.Public}
} else {
if memoFind.CreatorID == nil {
memoFind.CreatorID = &currentUserID
} else {
memoFind.VisibilityList = []api.Visibility{api.Public, api.Protected}
userID, ok := c.Get(getUserIDContextKey()).(int)
if !ok {
return echo.NewHTTPError(http.StatusBadRequest, "Missing user id to find tag")
}
memoFind.CreatorID = &userID
}
memoList, err := s.Store.FindMemoList(ctx, &memoFind)
// Only can get PUBLIC memos in visitor mode
_, ok := c.Get(getUserIDContextKey()).(int)
if !ok {
publicVisibility := api.Public
memoFind.Visibility = &publicVisibility
}
memoList, err := s.Store.FindMemoList(&memoFind)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find memo list").SetInternal(err)
}
tagMapSet := make(map[string]bool)
r, err := regexp.Compile("#(.+?) ")
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to compile regexp").SetInternal(err)
}
for _, memo := range memoList {
for _, tag := range findTagListFromMemoContent(memo.Content) {
for _, rawTag := range r.FindAllString(memo.Content, -1) {
tag := r.ReplaceAllString(rawTag, "$1")
tagMapSet[tag] = true
}
}
tagList := []string{}
for tag := range tagMapSet {
tagList = append(tagList, tag)
}
sort.Strings(tagList)
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
@@ -66,20 +71,3 @@ func (s *Server) registerTagRoutes(g *echo.Group) {
return nil
})
}
func findTagListFromMemoContent(memoContent string) []string {
tagMapSet := make(map[string]bool)
for _, tagRegexp := range tagRegexpList {
for _, rawTag := range tagRegexp.FindAllString(memoContent, -1) {
tag := tagRegexp.ReplaceAllString(rawTag, "$1")
tagMapSet[tag] = true
}
}
tagList := []string{}
for tag := range tagMapSet {
tagList = append(tagList, tag)
}
sort.Strings(tagList)
return tagList
}
-47
View File
@@ -1,47 +0,0 @@
package server
import (
"testing"
)
func TestFindTagListFromMemoContent(t *testing.T) {
tests := []struct {
memoContent string
want []string
}{
{
memoContent: "#tag1 ",
want: []string{"tag1"},
},
{
memoContent: "#tag1 #tag2 ",
want: []string{"tag1", "tag2"},
},
{
memoContent: "#tag1 #tag2 \n#tag3 ",
want: []string{"tag1", "tag2", "tag3"},
},
{
memoContent: "#tag1 #tag2 \n#tag3 #tag4 ",
want: []string{"tag1", "tag2", "tag3", "tag4"},
},
{
memoContent: "#tag1 #tag2 \n#tag3 #tag4 ",
want: []string{"tag1", "tag2", "tag3", "tag4"},
},
{
memoContent: "#tag1 123123#tag2 \n#tag3 #tag4 ",
want: []string{"tag1", "tag3", "tag4"},
},
{
memoContent: "#tag1 http://123123.com?123123#tag2 \n#tag3 #tag4 http://123123.com?123123#tag2) ",
want: []string{"tag1", "tag3", "tag4"},
},
}
for _, test := range tests {
result := findTagListFromMemoContent(test.memoContent)
if len(result) != len(test.want) {
t.Errorf("Find tag list %s: got result %v, want %v.", test.memoContent, result, test.want)
}
}
}
+57 -140
View File
@@ -5,11 +5,9 @@ import (
"fmt"
"net/http"
"strconv"
"time"
"github.com/usememos/memos/api"
"github.com/usememos/memos/common"
metric "github.com/usememos/memos/plugin/metrics"
"github.com/labstack/echo/v4"
"golang.org/x/crypto/bcrypt"
@@ -17,45 +15,21 @@ import (
func (s *Server) registerUserRoutes(g *echo.Group) {
g.POST("/user", func(c echo.Context) error {
ctx := c.Request().Context()
userID, ok := c.Get(getUserIDContextKey()).(int)
if !ok {
return echo.NewHTTPError(http.StatusUnauthorized, "Missing auth session")
}
currentUser, err := s.Store.FindUser(ctx, &api.UserFind{
ID: &userID,
})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user by id").SetInternal(err)
}
if currentUser.Role != api.Host {
return echo.NewHTTPError(http.StatusUnauthorized, "Only Host user can create member.")
}
userCreate := &api.UserCreate{
OpenID: common.GenUUID(),
}
userCreate := &api.UserCreate{}
if err := json.NewDecoder(c.Request().Body).Decode(userCreate); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post user request").SetInternal(err)
}
if err := userCreate.Validate(); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Invalid user create format.").SetInternal(err)
}
passwordHash, err := bcrypt.GenerateFromPassword([]byte(userCreate.Password), bcrypt.DefaultCost)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to generate password hash").SetInternal(err)
}
userCreate.PasswordHash = string(passwordHash)
user, err := s.Store.CreateUser(ctx, userCreate)
user, err := s.Store.CreateUser(userCreate)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create user").SetInternal(err)
}
s.Collector.Collect(ctx, &metric.Metric{
Name: "user created",
})
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(user)); err != nil {
@@ -65,17 +39,11 @@ func (s *Server) registerUserRoutes(g *echo.Group) {
})
g.GET("/user", func(c echo.Context) error {
ctx := c.Request().Context()
userList, err := s.Store.FindUserList(ctx, &api.UserFind{})
userList, err := s.Store.FindUserList(&api.UserFind{})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch user list").SetInternal(err)
}
for _, user := range userList {
// data desensitize
user.OpenID = ""
}
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(userList)); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode user list response").SetInternal(err)
@@ -83,73 +51,13 @@ func (s *Server) registerUserRoutes(g *echo.Group) {
return nil
})
// GET /api/user/me is used to check if the user is logged in.
g.GET("/user/me", func(c echo.Context) error {
ctx := c.Request().Context()
userID, ok := c.Get(getUserIDContextKey()).(int)
if !ok {
return echo.NewHTTPError(http.StatusUnauthorized, "Missing auth session")
}
userFind := &api.UserFind{
ID: &userID,
}
user, err := s.Store.FindUser(ctx, userFind)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user").SetInternal(err)
}
userSettingList, err := s.Store.FindUserSettingList(ctx, &api.UserSettingFind{
UserID: userID,
})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find userSettingList").SetInternal(err)
}
user.UserSettingList = userSettingList
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(user)); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode user response").SetInternal(err)
}
return nil
})
g.POST("/user/setting", func(c echo.Context) error {
ctx := c.Request().Context()
userID, ok := c.Get(getUserIDContextKey()).(int)
if !ok {
return echo.NewHTTPError(http.StatusUnauthorized, "Missing auth session")
}
userSettingUpsert := &api.UserSettingUpsert{}
if err := json.NewDecoder(c.Request().Body).Decode(userSettingUpsert); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post user setting upsert request").SetInternal(err)
}
if err := userSettingUpsert.Validate(); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Invalid user setting format").SetInternal(err)
}
userSettingUpsert.UserID = userID
userSetting, err := s.Store.UpsertUserSetting(ctx, userSettingUpsert)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to upsert user setting").SetInternal(err)
}
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(userSetting)); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode user setting response").SetInternal(err)
}
return nil
})
g.GET("/user/:id", func(c echo.Context) error {
ctx := c.Request().Context()
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted user id").SetInternal(err)
}
user, err := s.Store.FindUser(ctx, &api.UserFind{
user, err := s.Store.FindUser(&api.UserFind{
ID: &id,
})
if err != nil {
@@ -168,41 +76,38 @@ func (s *Server) registerUserRoutes(g *echo.Group) {
return nil
})
g.PATCH("/user/:id", func(c echo.Context) error {
ctx := c.Request().Context()
userID, err := strconv.Atoi(c.Param("id"))
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("id"))).SetInternal(err)
}
currentUserID, ok := c.Get(getUserIDContextKey()).(int)
if !ok {
return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
}
currentUser, err := s.Store.FindUser(ctx, &api.UserFind{
ID: &currentUserID,
})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user").SetInternal(err)
}
if currentUser == nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Current session user not found with ID: %d", currentUserID)).SetInternal(err)
} else if currentUser.Role != api.Host && currentUserID != userID {
return echo.NewHTTPError(http.StatusForbidden, "Access forbidden for current session user").SetInternal(err)
// GET /api/user/me is used to check if the user is logged in.
g.GET("/user/me", func(c echo.Context) error {
userSessionID := c.Get(getUserIDContextKey())
if userSessionID == nil {
return echo.NewHTTPError(http.StatusUnauthorized, "Missing auth session")
}
currentTs := time.Now().Unix()
userID := userSessionID.(int)
userFind := &api.UserFind{
ID: &userID,
}
user, err := s.Store.FindUser(userFind)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch user").SetInternal(err)
}
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(user)); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode user response").SetInternal(err)
}
return nil
})
g.PATCH("/user/me", func(c echo.Context) error {
userID := c.Get(getUserIDContextKey()).(int)
userPatch := &api.UserPatch{
ID: userID,
UpdatedTs: &currentTs,
ID: userID,
}
if err := json.NewDecoder(c.Request().Body).Decode(userPatch); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted patch user request").SetInternal(err)
}
if userPatch.Email != nil && *userPatch.Email != "" && !common.ValidateEmail(*userPatch.Email) {
return echo.NewHTTPError(http.StatusBadRequest, "Invalid email format")
}
if userPatch.Password != nil && *userPatch.Password != "" {
passwordHash, err := bcrypt.GenerateFromPassword([]byte(*userPatch.Password), bcrypt.DefaultCost)
if err != nil {
@@ -218,7 +123,7 @@ func (s *Server) registerUserRoutes(g *echo.Group) {
userPatch.OpenID = &openID
}
user, err := s.Store.PatchUser(ctx, userPatch)
user, err := s.Store.PatchUser(userPatch)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to patch user").SetInternal(err)
}
@@ -230,13 +135,9 @@ func (s *Server) registerUserRoutes(g *echo.Group) {
return nil
})
g.DELETE("/user/:id", func(c echo.Context) error {
ctx := c.Request().Context()
currentUserID, ok := c.Get(getUserIDContextKey()).(int)
if !ok {
return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
}
currentUser, err := s.Store.FindUser(ctx, &api.UserFind{
g.PATCH("/user/:userId", func(c echo.Context) error {
currentUserID := c.Get(getUserIDContextKey()).(int)
currentUser, err := s.Store.FindUser(&api.UserFind{
ID: &currentUserID,
})
if err != nil {
@@ -248,21 +149,37 @@ func (s *Server) registerUserRoutes(g *echo.Group) {
return echo.NewHTTPError(http.StatusForbidden, "Access forbidden for current session user").SetInternal(err)
}
userID, err := strconv.Atoi(c.Param("id"))
userID, err := strconv.Atoi(c.Param("userId"))
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("id"))).SetInternal(err)
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("userId"))).SetInternal(err)
}
userDelete := &api.UserDelete{
userPatch := &api.UserPatch{
ID: userID,
}
if err := s.Store.DeleteUser(ctx, userDelete); err != nil {
if common.ErrorCode(err) == common.NotFound {
return echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("User ID not found: %d", userID))
}
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to delete user").SetInternal(err)
if err := json.NewDecoder(c.Request().Body).Decode(userPatch); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted patch user request").SetInternal(err)
}
return c.JSON(http.StatusOK, true)
if userPatch.Password != nil && *userPatch.Password != "" {
passwordHash, err := bcrypt.GenerateFromPassword([]byte(*userPatch.Password), bcrypt.DefaultCost)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to generate password hash").SetInternal(err)
}
passwordHashStr := string(passwordHash)
userPatch.PasswordHash = &passwordHashStr
}
user, err := s.Store.PatchUser(userPatch)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to patch user").SetInternal(err)
}
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(user)); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode user response").SetInternal(err)
}
return nil
})
}
+42
View File
@@ -0,0 +1,42 @@
package server
import (
"fmt"
"net/http"
"strconv"
"github.com/usememos/memos/api"
"github.com/labstack/echo/v4"
)
func (s *Server) registerWebhookRoutes(g *echo.Group) {
g.GET("/test", func(c echo.Context) error {
return c.HTML(http.StatusOK, "<strong>Hello, World!</strong>")
})
g.GET("/r/:resourceId/:filename", func(c echo.Context) error {
resourceID, err := strconv.Atoi(c.Param("resourceId"))
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("resourceId"))).SetInternal(err)
}
filename := c.Param("filename")
resourceFind := &api.ResourceFind{
ID: &resourceID,
Filename: &filename,
}
resource, err := s.Store.FindResource(resourceFind)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to fetch resource ID: %v", resourceID)).SetInternal(err)
}
c.Response().Writer.WriteHeader(http.StatusOK)
c.Response().Writer.Header().Set("Content-Type", resource.Type)
if _, err := c.Response().Writer.Write(resource.Blob); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to write response").SetInternal(err)
}
return nil
})
}
-72
View File
@@ -1,72 +0,0 @@
package store
import (
"bytes"
"encoding/binary"
"encoding/gob"
"fmt"
"github.com/VictoriaMetrics/fastcache"
"github.com/usememos/memos/api"
)
var (
// 64 MiB.
cacheSize = 1024 * 1024 * 64
_ api.CacheService = (*CacheService)(nil)
)
// CacheService implements a cache.
type CacheService struct {
cache *fastcache.Cache
}
// NewCacheService creates a cache service.
func NewCacheService() *CacheService {
return &CacheService{
cache: fastcache.New(cacheSize),
}
}
// FindCache finds the value in cache.
func (s *CacheService) FindCache(namespace api.CacheNamespace, id int, entry interface{}) (bool, error) {
buf1 := []byte{0, 0, 0, 0, 0, 0, 0, 0}
binary.LittleEndian.PutUint64(buf1, uint64(id))
buf2, has := s.cache.HasGet(nil, append([]byte(namespace), buf1...))
if has {
dec := gob.NewDecoder(bytes.NewReader(buf2))
if err := dec.Decode(entry); err != nil {
return false, fmt.Errorf("failed to decode entry for cache namespace: %s, error: %w", namespace, err)
}
return true, nil
}
return false, nil
}
// UpsertCache upserts the value to cache.
func (s *CacheService) UpsertCache(namespace api.CacheNamespace, id int, entry interface{}) error {
buf1 := []byte{0, 0, 0, 0, 0, 0, 0, 0}
binary.LittleEndian.PutUint64(buf1, uint64(id))
var buf2 bytes.Buffer
enc := gob.NewEncoder(&buf2)
if err := enc.Encode(entry); err != nil {
return fmt.Errorf("failed to encode entry for cache namespace: %s, error: %w", namespace, err)
}
s.cache.Set(append([]byte(namespace), buf1...), buf2.Bytes())
return nil
}
// DeleteCache deletes the cache.
func (s *CacheService) DeleteCache(namespace api.CacheNamespace, id int) {
buf1 := []byte{0, 0, 0, 0, 0, 0, 0, 0}
binary.LittleEndian.PutUint64(buf1, uint64(id))
_, has := s.cache.HasGet(nil, append([]byte(namespace), buf1...))
if has {
s.cache.Del(append([]byte(namespace), buf1...))
}
}
+57 -43
View File
@@ -1,19 +1,21 @@
package db
import (
"context"
"database/sql"
"embed"
"errors"
"fmt"
"io/fs"
"io/ioutil"
"os"
"regexp"
"sort"
"time"
"github.com/usememos/memos/common"
"github.com/usememos/memos/server/profile"
"github.com/usememos/memos/server/version"
_ "github.com/mattn/go-sqlite3"
)
//go:embed migration
@@ -36,43 +38,47 @@ func NewDB(profile *profile.Profile) *DB {
return db
}
func (db *DB) Open(ctx context.Context) (err error) {
func (db *DB) Open() (err error) {
// Ensure a DSN is set before attempting to open the database.
if db.profile.DSN == "" {
return fmt.Errorf("dsn required")
}
// Connect to the database without foreign_key.
sqlDB, err := sql.Open("sqlite3", db.profile.DSN+"?_foreign_keys=0")
// Connect to the database.
sqlDB, err := sql.Open("sqlite3", db.profile.DSN)
if err != nil {
return fmt.Errorf("failed to open db with dsn: %s, err: %w", db.profile.DSN, err)
}
db.Db = sqlDB
db.Db = sqlDB
// If mode is dev, we should migrate and seed the database.
if db.profile.Mode == "dev" {
if _, err := os.Stat(db.profile.DSN); errors.Is(err, os.ErrNotExist) {
if err := db.applyLatestSchema(ctx); err != nil {
return fmt.Errorf("failed to apply latest schema: %w", err)
}
if err := db.seed(ctx); err != nil {
return fmt.Errorf("failed to seed: %w", err)
}
if err := db.applyLatestSchema(); err != nil {
return fmt.Errorf("failed to apply latest schema: %w", err)
}
if err := db.seed(); err != nil {
return fmt.Errorf("failed to seed: %w", err)
}
} else {
// If db file not exists, we should migrate the database.
if _, err := os.Stat(db.profile.DSN); errors.Is(err, os.ErrNotExist) {
if err := db.applyLatestSchema(ctx); err != nil {
err := db.applyLatestSchema()
if err != nil {
return fmt.Errorf("failed to apply latest schema: %w", err)
}
} else {
currentVersion := version.GetCurrentVersion(db.profile.Mode)
migrationHistory, err := db.FindMigrationHistory(ctx, &MigrationHistoryFind{})
err := db.createMigrationHistoryTable()
if err != nil {
return fmt.Errorf("failed to create migration_history table: %w", err)
}
currentVersion := common.GetCurrentVersion(db.profile.Mode)
migrationHistory, err := findMigrationHistory(db.Db, &MigrationHistoryFind{})
if err != nil {
return err
}
if migrationHistory == nil {
migrationHistory, err = db.UpsertMigrationHistory(ctx, &MigrationHistoryUpsert{
migrationHistory, err = upsertMigrationHistory(db.Db, &MigrationHistoryUpsert{
Version: currentVersion,
})
if err != nil {
@@ -80,16 +86,16 @@ func (db *DB) Open(ctx context.Context) (err error) {
}
}
if version.IsVersionGreaterThan(version.GetSchemaVersion(currentVersion), migrationHistory.Version) {
if common.IsVersionGreaterThan(currentVersion, migrationHistory.Version) {
minorVersionList := getMinorVersionList()
// backup the raw database file before migration
rawBytes, err := os.ReadFile(db.profile.DSN)
rawBytes, err := ioutil.ReadFile(db.profile.DSN)
if err != nil {
return fmt.Errorf("failed to read raw database file, err: %w", err)
}
backupDBFilePath := fmt.Sprintf("%s/memos_%s_%d_backup.db", db.profile.Data, db.profile.Version, time.Now().Unix())
if err := os.WriteFile(backupDBFilePath, rawBytes, 0644); err != nil {
if err := ioutil.WriteFile(backupDBFilePath, rawBytes, 0644); err != nil {
return fmt.Errorf("failed to write raw database file, err: %w", err)
}
@@ -97,9 +103,9 @@ func (db *DB) Open(ctx context.Context) (err error) {
println("start migrate")
for _, minorVersion := range minorVersionList {
normalizedVersion := minorVersion + ".0"
if version.IsVersionGreaterThan(normalizedVersion, migrationHistory.Version) && version.IsVersionGreaterOrEqualThan(currentVersion, normalizedVersion) {
if common.IsVersionGreaterThan(normalizedVersion, migrationHistory.Version) && common.IsVersionGreaterOrEqualThan(currentVersion, normalizedVersion) {
println("applying migration for", normalizedVersion)
if err := db.applyMigrationForMinorVersion(ctx, minorVersion); err != nil {
if err := db.applyMigrationForMinorVersion(minorVersion); err != nil {
return fmt.Errorf("failed to apply minor version migration: %w", err)
}
}
@@ -114,28 +120,28 @@ func (db *DB) Open(ctx context.Context) (err error) {
}
}
return nil
return err
}
const (
latestSchemaFileName = "LATEST__SCHEMA.sql"
)
func (db *DB) applyLatestSchema(ctx context.Context) error {
latestSchemaPath := fmt.Sprintf("%s/%s/%s", "migration", db.profile.Mode, latestSchemaFileName)
func (db *DB) applyLatestSchema() error {
latestSchemaPath := fmt.Sprintf("%s/%s", "migration", latestSchemaFileName)
buf, err := migrationFS.ReadFile(latestSchemaPath)
if err != nil {
return fmt.Errorf("failed to read latest schema %q, error %w", latestSchemaPath, err)
}
stmt := string(buf)
if err := db.execute(ctx, stmt); err != nil {
if err := db.execute(stmt); err != nil {
return fmt.Errorf("migrate error: statement:%s err=%w", stmt, err)
}
return nil
}
func (db *DB) applyMigrationForMinorVersion(ctx context.Context, minorVersion string) error {
filenames, err := fs.Glob(migrationFS, fmt.Sprintf("%s/%s/*.sql", "migration/prod", minorVersion))
func (db *DB) applyMigrationForMinorVersion(minorVersion string) error {
filenames, err := fs.Glob(migrationFS, fmt.Sprintf("%s/%s/*.sql", "migration", minorVersion))
if err != nil {
return err
}
@@ -151,28 +157,22 @@ func (db *DB) applyMigrationForMinorVersion(ctx context.Context, minorVersion st
}
stmt := string(buf)
migrationStmt += stmt
if err := db.execute(ctx, stmt); err != nil {
if err := db.execute(stmt); err != nil {
return fmt.Errorf("migrate error: statement:%s err=%w", stmt, err)
}
}
tx, err := db.Db.Begin()
if err != nil {
return err
}
defer tx.Rollback()
// upsert the newest version to migration_history
if _, err = upsertMigrationHistory(ctx, tx, &MigrationHistoryUpsert{
if _, err = upsertMigrationHistory(db.Db, &MigrationHistoryUpsert{
Version: minorVersion + ".0",
}); err != nil {
return err
}
return tx.Commit()
return nil
}
func (db *DB) seed(ctx context.Context) error {
func (db *DB) seed() error {
filenames, err := fs.Glob(seedFS, fmt.Sprintf("%s/*.sql", "seed"))
if err != nil {
return err
@@ -187,22 +187,22 @@ func (db *DB) seed(ctx context.Context) error {
return fmt.Errorf("failed to read seed file, filename=%s err=%w", filename, err)
}
stmt := string(buf)
if err := db.execute(ctx, stmt); err != nil {
if err := db.execute(stmt); err != nil {
return fmt.Errorf("seed error: statement:%s err=%w", stmt, err)
}
}
return nil
}
// execute runs a single SQL statement within a transaction.
func (db *DB) execute(ctx context.Context, stmt string) error {
// excecute runs a single SQL statement within a transaction.
func (db *DB) execute(stmt string) error {
tx, err := db.Db.Begin()
if err != nil {
return err
}
defer tx.Rollback()
if _, err := tx.ExecContext(ctx, stmt); err != nil {
if _, err := tx.Exec(stmt); err != nil {
return err
}
@@ -210,7 +210,7 @@ func (db *DB) execute(ctx context.Context, stmt string) error {
}
// minorDirRegexp is a regular expression for minor version directory.
var minorDirRegexp = regexp.MustCompile(`^migration/prod/[0-9]+\.[0-9]+$`)
var minorDirRegexp = regexp.MustCompile(`^migration/[0-9]+\.[0-9]+$`)
func getMinorVersionList() []string {
minorVersionList := []string{}
@@ -232,3 +232,17 @@ func getMinorVersionList() []string {
return minorVersionList
}
// createMigrationHistoryTable creates the migration_history table if it doesn't exist.
func (db *DB) createMigrationHistoryTable() error {
if err := createTable(db.Db, `
CREATE TABLE IF NOT EXISTS migration_history (
version TEXT NOT NULL PRIMARY KEY,
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now'))
);
`); err != nil {
return err
}
return nil
}
@@ -4,7 +4,8 @@ PRAGMA foreign_keys = off;
DROP TABLE IF EXISTS _user_old;
ALTER TABLE user RENAME TO _user_old;
ALTER TABLE
user RENAME TO _user_old;
CREATE TABLE user (
id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -0,0 +1 @@
ALTER TABLE memo ADD visibility TEXT NOT NULL CHECK (visibility IN ('PUBLIC', 'PRIVATE')) DEFAULT 'PRIVATE';
@@ -1,14 +1,16 @@
PRAGMA foreign_keys=off;
DROP TABLE IF EXISTS _user_old;
ALTER TABLE user RENAME TO _user_old;
-- drop all tables
DROP TABLE IF EXISTS `memo_organizer`;
DROP TABLE IF EXISTS `memo`;
DROP TABLE IF EXISTS `shortcut`;
DROP TABLE IF EXISTS `resource`;
DROP TABLE IF EXISTS `user`;
-- user
CREATE TABLE user (
id INTEGER PRIMARY KEY AUTOINCREMENT,
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
-- allowed row status are 'NORMAL', 'ARCHIVED'.
row_status TEXT NOT NULL CHECK (row_status IN ('NORMAL', 'ARCHIVED')) DEFAULT 'NORMAL',
email TEXT NOT NULL UNIQUE,
role TEXT NOT NULL CHECK (role IN ('HOST', 'USER')) DEFAULT 'USER',
@@ -17,11 +19,10 @@ CREATE TABLE user (
open_id TEXT NOT NULL UNIQUE
);
INSERT INTO user SELECT * FROM _user_old;
DROP TABLE IF EXISTS _user_old;
DROP TRIGGER IF EXISTS `trigger_update_user_modification_time`;
INSERT INTO
sqlite_sequence (name, seq)
VALUES
('user', 100);
CREATE TRIGGER IF NOT EXISTS `trigger_update_user_modification_time`
AFTER
@@ -35,10 +36,6 @@ WHERE
rowid = old.rowid;
END;
DROP TABLE IF EXISTS _memo_old;
ALTER TABLE memo RENAME TO _memo_old;
-- memo
CREATE TABLE memo (
id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -47,15 +44,14 @@ CREATE TABLE memo (
updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
row_status TEXT NOT NULL CHECK (row_status IN ('NORMAL', 'ARCHIVED')) DEFAULT 'NORMAL',
content TEXT NOT NULL DEFAULT '',
visibility TEXT NOT NULL CHECK (visibility IN ('PUBLIC', 'PROTECTED', 'PRIVATE')) DEFAULT 'PRIVATE',
visibility TEXT NOT NULL CHECK (visibility IN ('PUBLIC', 'PRIVATE')) DEFAULT 'PRIVATE',
FOREIGN KEY(creator_id) REFERENCES user(id) ON DELETE CASCADE
);
INSERT INTO memo SELECT * FROM _memo_old;
DROP TABLE IF EXISTS _memo_old;
DROP TRIGGER IF EXISTS `trigger_update_memo_modification_time`;
INSERT INTO
sqlite_sequence (name, seq)
VALUES
('memo', 1000);
CREATE TRIGGER IF NOT EXISTS `trigger_update_memo_modification_time`
AFTER
@@ -69,10 +65,6 @@ WHERE
rowid = old.rowid;
END;
DROP TABLE IF EXISTS _memo_organizer_old;
ALTER TABLE memo_organizer RENAME TO _memo_organizer_old;
-- memo_organizer
CREATE TABLE memo_organizer (
id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -84,13 +76,10 @@ CREATE TABLE memo_organizer (
UNIQUE(memo_id, user_id)
);
INSERT INTO memo_organizer SELECT * FROM _memo_organizer_old;
DROP TABLE IF EXISTS _memo_organizer_old;
DROP TABLE IF EXISTS _shortcut_old;
ALTER TABLE shortcut RENAME TO _shortcut_old;
INSERT INTO
sqlite_sequence (name, seq)
VALUES
('memo_organizer', 1000);
-- shortcut
CREATE TABLE shortcut (
@@ -104,11 +93,10 @@ CREATE TABLE shortcut (
FOREIGN KEY(creator_id) REFERENCES user(id) ON DELETE CASCADE
);
INSERT INTO shortcut SELECT * FROM _shortcut_old;
DROP TABLE IF EXISTS _shortcut_old;
DROP TRIGGER IF EXISTS `trigger_update_shortcut_modification_time`;
INSERT INTO
sqlite_sequence (name, seq)
VALUES
('shortcut', 10000);
CREATE TRIGGER IF NOT EXISTS `trigger_update_shortcut_modification_time`
AFTER
@@ -122,10 +110,6 @@ WHERE
rowid = old.rowid;
END;
DROP TABLE IF EXISTS _resource_old;
ALTER TABLE resource RENAME TO _resource_old;
-- resource
CREATE TABLE resource (
id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -133,17 +117,16 @@ CREATE TABLE resource (
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
filename TEXT NOT NULL DEFAULT '',
blob BLOB DEFAULT NULL,
blob BLOB NOT NULL,
type TEXT NOT NULL DEFAULT '',
size INTEGER NOT NULL DEFAULT 0,
FOREIGN KEY(creator_id) REFERENCES user(id) ON DELETE CASCADE
);
INSERT INTO resource SELECT * FROM _resource_old;
DROP TABLE IF EXISTS _resource_old;
DROP TRIGGER IF EXISTS `trigger_update_resource_modification_time`;
INSERT INTO
sqlite_sequence (name, seq)
VALUES
('resource', 10000);
CREATE TRIGGER IF NOT EXISTS `trigger_update_resource_modification_time`
AFTER
@@ -156,22 +139,3 @@ SET
WHERE
rowid = old.rowid;
END;
DROP TABLE IF EXISTS _user_setting_old;
ALTER TABLE user_setting RENAME TO _user_setting_old;
-- user_setting
CREATE TABLE user_setting (
user_id INTEGER NOT NULL,
key TEXT NOT NULL,
value TEXT NOT NULL,
FOREIGN KEY(user_id) REFERENCES user(id) ON DELETE CASCADE,
UNIQUE(user_id, key)
);
INSERT INTO user_setting SELECT * FROM _user_setting_old;
DROP TABLE IF EXISTS _user_setting_old;
PRAGMA foreign_keys=on;
-88
View File
@@ -1,88 +0,0 @@
-- migration_history
CREATE TABLE migration_history (
version TEXT NOT NULL PRIMARY KEY,
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now'))
);
-- system_setting
CREATE TABLE system_setting (
name TEXT NOT NULL,
value TEXT NOT NULL,
description TEXT NOT NULL DEFAULT '',
UNIQUE(name)
);
-- user
CREATE TABLE user (
id INTEGER PRIMARY KEY AUTOINCREMENT,
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
row_status TEXT NOT NULL CHECK (row_status IN ('NORMAL', 'ARCHIVED')) DEFAULT 'NORMAL',
username TEXT NOT NULL UNIQUE,
role TEXT NOT NULL CHECK (role IN ('HOST', 'ADMIN', 'USER')) DEFAULT 'USER',
email TEXT NOT NULL DEFAULT '',
nickname TEXT NOT NULL DEFAULT '',
password_hash TEXT NOT NULL,
open_id TEXT NOT NULL UNIQUE
);
-- user_setting
CREATE TABLE user_setting (
user_id INTEGER NOT NULL,
key TEXT NOT NULL,
value TEXT NOT NULL,
UNIQUE(user_id, key)
);
-- memo
CREATE TABLE memo (
id INTEGER PRIMARY KEY AUTOINCREMENT,
creator_id INTEGER NOT NULL,
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
row_status TEXT NOT NULL CHECK (row_status IN ('NORMAL', 'ARCHIVED')) DEFAULT 'NORMAL',
content TEXT NOT NULL DEFAULT '',
visibility TEXT NOT NULL CHECK (visibility IN ('PUBLIC', 'PROTECTED', 'PRIVATE')) DEFAULT 'PRIVATE'
);
-- memo_organizer
CREATE TABLE memo_organizer (
id INTEGER PRIMARY KEY AUTOINCREMENT,
memo_id INTEGER NOT NULL,
user_id INTEGER NOT NULL,
pinned INTEGER NOT NULL CHECK (pinned IN (0, 1)) DEFAULT 0,
UNIQUE(memo_id, user_id)
);
-- shortcut
CREATE TABLE shortcut (
id INTEGER PRIMARY KEY AUTOINCREMENT,
creator_id INTEGER NOT NULL,
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
row_status TEXT NOT NULL CHECK (row_status IN ('NORMAL', 'ARCHIVED')) DEFAULT 'NORMAL',
title TEXT NOT NULL DEFAULT '',
payload TEXT NOT NULL DEFAULT '{}'
);
-- resource
CREATE TABLE resource (
id INTEGER PRIMARY KEY AUTOINCREMENT,
creator_id INTEGER NOT NULL,
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
filename TEXT NOT NULL DEFAULT '',
blob BLOB DEFAULT NULL,
external_link TEXT NOT NULL DEFAULT '',
type TEXT NOT NULL DEFAULT '',
size INTEGER NOT NULL DEFAULT 0
);
-- memo_resource
CREATE TABLE memo_resource (
memo_id INTEGER NOT NULL,
resource_id INTEGER NOT NULL,
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
UNIQUE(memo_id, resource_id)
);
@@ -1 +0,0 @@
ALTER TABLE memo ADD COLUMN visibility TEXT NOT NULL CHECK (visibility IN ('PUBLIC', 'PRIVATE')) DEFAULT 'PRIVATE';
@@ -1,37 +0,0 @@
-- change memo visibility field from "PRIVATE"/"PUBLIC" to "PRIVATE"/"PROTECTED"/"PUBLIC".
PRAGMA foreign_keys = off;
DROP TABLE IF EXISTS _memo_old;
ALTER TABLE memo RENAME TO _memo_old;
CREATE TABLE memo (
id INTEGER PRIMARY KEY AUTOINCREMENT,
creator_id INTEGER NOT NULL,
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
row_status TEXT NOT NULL CHECK (row_status IN ('NORMAL', 'ARCHIVED')) DEFAULT 'NORMAL',
content TEXT NOT NULL DEFAULT '',
visibility TEXT NOT NULL CHECK (visibility IN ('PUBLIC', 'PROTECTED', 'PRIVATE')) DEFAULT 'PRIVATE',
FOREIGN KEY(creator_id) REFERENCES user(id) ON DELETE CASCADE
);
INSERT INTO memo (
id, creator_id, created_ts, updated_ts,
row_status, content, visibility
)
SELECT
id,
creator_id,
created_ts,
updated_ts,
row_status,
content,
visibility
FROM
_memo_old;
DROP TABLE IF EXISTS _memo_old;
PRAGMA foreign_keys = on;
@@ -1,9 +0,0 @@
-- user_setting
CREATE TABLE user_setting (
user_id INTEGER NOT NULL,
key TEXT NOT NULL,
value TEXT NOT NULL,
FOREIGN KEY(user_id) REFERENCES user(id) ON DELETE CASCADE
);
CREATE UNIQUE INDEX user_setting_key_user_id_index ON user_setting(key, user_id);
@@ -1,10 +0,0 @@
-- memo_resource
CREATE TABLE memo_resource (
memo_id INTEGER NOT NULL,
resource_id INTEGER NOT NULL,
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
FOREIGN KEY(memo_id) REFERENCES memo(id) ON DELETE CASCADE,
FOREIGN KEY(resource_id) REFERENCES resource(id) ON DELETE CASCADE,
UNIQUE(memo_id, resource_id)
);
@@ -1,7 +0,0 @@
-- system_setting
CREATE TABLE system_setting (
name TEXT NOT NULL,
value TEXT NOT NULL,
description TEXT NOT NULL DEFAULT '',
UNIQUE(name)
);
@@ -1 +0,0 @@
ALTER TABLE resource ADD COLUMN external_link TEXT NOT NULL DEFAULT '';
@@ -1,55 +0,0 @@
DROP TRIGGER IF EXISTS `trigger_update_user_modification_time`;
CREATE TRIGGER IF NOT EXISTS `trigger_update_user_modification_time`
AFTER
UPDATE
ON `user` FOR EACH ROW BEGIN
UPDATE
`user`
SET
updated_ts = (strftime('%s', 'now'))
WHERE
rowid = old.rowid;
END;
DROP TRIGGER IF EXISTS `trigger_update_memo_modification_time`;
CREATE TRIGGER IF NOT EXISTS `trigger_update_memo_modification_time`
AFTER
UPDATE
ON `memo` FOR EACH ROW BEGIN
UPDATE
`memo`
SET
updated_ts = (strftime('%s', 'now'))
WHERE
rowid = old.rowid;
END;
DROP TRIGGER IF EXISTS `trigger_update_shortcut_modification_time`;
CREATE TRIGGER IF NOT EXISTS `trigger_update_shortcut_modification_time`
AFTER
UPDATE
ON `shortcut` FOR EACH ROW BEGIN
UPDATE
`shortcut`
SET
updated_ts = (strftime('%s', 'now'))
WHERE
rowid = old.rowid;
END;
DROP TRIGGER IF EXISTS `trigger_update_resource_modification_time`;
CREATE TRIGGER IF NOT EXISTS `trigger_update_resource_modification_time`
AFTER
UPDATE
ON `resource` FOR EACH ROW BEGIN
UPDATE
`resource`
SET
updated_ts = (strftime('%s', 'now'))
WHERE
rowid = old.rowid;
END;
@@ -1,147 +0,0 @@
PRAGMA foreign_keys=off;
DROP TABLE IF EXISTS _user_old;
ALTER TABLE user RENAME TO _user_old;
-- user
CREATE TABLE user (
id INTEGER PRIMARY KEY AUTOINCREMENT,
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
row_status TEXT NOT NULL CHECK (row_status IN ('NORMAL', 'ARCHIVED')) DEFAULT 'NORMAL',
email TEXT NOT NULL UNIQUE,
role TEXT NOT NULL CHECK (role IN ('HOST', 'USER')) DEFAULT 'USER',
name TEXT NOT NULL,
password_hash TEXT NOT NULL,
open_id TEXT NOT NULL UNIQUE
);
INSERT INTO user SELECT * FROM _user_old;
DROP TABLE IF EXISTS _user_old;
DROP TABLE IF EXISTS _memo_old;
ALTER TABLE memo RENAME TO _memo_old;
-- memo
CREATE TABLE memo (
id INTEGER PRIMARY KEY AUTOINCREMENT,
creator_id INTEGER NOT NULL,
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
row_status TEXT NOT NULL CHECK (row_status IN ('NORMAL', 'ARCHIVED')) DEFAULT 'NORMAL',
content TEXT NOT NULL DEFAULT '',
visibility TEXT NOT NULL CHECK (visibility IN ('PUBLIC', 'PROTECTED', 'PRIVATE')) DEFAULT 'PRIVATE'
);
INSERT INTO memo SELECT * FROM _memo_old;
DROP TABLE IF EXISTS _memo_old;
DROP TABLE IF EXISTS _memo_organizer_old;
ALTER TABLE memo_organizer RENAME TO _memo_organizer_old;
-- memo_organizer
CREATE TABLE memo_organizer (
id INTEGER PRIMARY KEY AUTOINCREMENT,
memo_id INTEGER NOT NULL,
user_id INTEGER NOT NULL,
pinned INTEGER NOT NULL CHECK (pinned IN (0, 1)) DEFAULT 0,
UNIQUE(memo_id, user_id)
);
INSERT INTO memo_organizer SELECT * FROM _memo_organizer_old;
DROP TABLE IF EXISTS _memo_organizer_old;
DROP TABLE IF EXISTS _shortcut_old;
ALTER TABLE shortcut RENAME TO _shortcut_old;
-- shortcut
CREATE TABLE shortcut (
id INTEGER PRIMARY KEY AUTOINCREMENT,
creator_id INTEGER NOT NULL,
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
row_status TEXT NOT NULL CHECK (row_status IN ('NORMAL', 'ARCHIVED')) DEFAULT 'NORMAL',
title TEXT NOT NULL DEFAULT '',
payload TEXT NOT NULL DEFAULT '{}'
);
INSERT INTO shortcut SELECT * FROM _shortcut_old;
DROP TABLE IF EXISTS _shortcut_old;
DROP TABLE IF EXISTS _resource_old;
ALTER TABLE resource RENAME TO _resource_old;
-- resource
CREATE TABLE resource (
id INTEGER PRIMARY KEY AUTOINCREMENT,
creator_id INTEGER NOT NULL,
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
filename TEXT NOT NULL DEFAULT '',
blob BLOB DEFAULT NULL,
external_link TEXT NOT NULL DEFAULT '',
type TEXT NOT NULL DEFAULT '',
size INTEGER NOT NULL DEFAULT 0
);
INSERT INTO resource (
id, creator_id, created_ts, updated_ts,
filename, blob, external_link, type,
size
)
SELECT
id,
creator_id,
created_ts,
updated_ts,
filename,
blob,
external_link,
type,
size
FROM
_resource_old;
DROP TABLE IF EXISTS _resource_old;
DROP TABLE IF EXISTS _user_setting_old;
ALTER TABLE user_setting RENAME TO _user_setting_old;
-- user_setting
CREATE TABLE user_setting (
user_id INTEGER NOT NULL,
key TEXT NOT NULL,
value TEXT NOT NULL,
UNIQUE(user_id, key)
);
INSERT INTO user_setting SELECT * FROM _user_setting_old;
DROP TABLE IF EXISTS _user_setting_old;
DROP TABLE IF EXISTS _memo_resource_old;
ALTER TABLE memo_resource RENAME TO _memo_resource_old;
-- memo_resource
CREATE TABLE memo_resource (
memo_id INTEGER NOT NULL,
resource_id INTEGER NOT NULL,
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
UNIQUE(memo_id, resource_id)
);
INSERT INTO memo_resource SELECT * FROM _memo_resource_old;
DROP TABLE IF EXISTS _memo_resource_old;
@@ -1,4 +0,0 @@
DROP TRIGGER IF EXISTS `trigger_update_user_modification_time`;
DROP TRIGGER IF EXISTS `trigger_update_memo_modification_time`;
DROP TRIGGER IF EXISTS `trigger_update_shortcut_modification_time`;
DROP TRIGGER IF EXISTS `trigger_update_resource_modification_time`;
@@ -1,5 +0,0 @@
-- migration_history
CREATE TABLE IF NOT EXISTS migration_history (
version TEXT NOT NULL PRIMARY KEY,
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now'))
);
@@ -1,41 +0,0 @@
-- add column username TEXT NOT NULL UNIQUE
-- rename column name to nickname
-- add role `ADMIN`
DROP TABLE IF EXISTS _user_old;
ALTER TABLE user RENAME TO _user_old;
-- user
CREATE TABLE user (
id INTEGER PRIMARY KEY AUTOINCREMENT,
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
row_status TEXT NOT NULL CHECK (row_status IN ('NORMAL', 'ARCHIVED')) DEFAULT 'NORMAL',
username TEXT NOT NULL UNIQUE,
role TEXT NOT NULL CHECK (role IN ('HOST', 'ADMIN', 'USER')) DEFAULT 'USER',
email TEXT NOT NULL DEFAULT '',
nickname TEXT NOT NULL DEFAULT '',
password_hash TEXT NOT NULL,
open_id TEXT NOT NULL UNIQUE
);
INSERT INTO user (
id, created_ts, updated_ts, row_status,
username, role, email, nickname, password_hash,
open_id
)
SELECT
id,
created_ts,
updated_ts,
row_status,
email,
role,
email,
name,
password_hash,
open_id
FROM
_user_old;
DROP TABLE IF EXISTS _user_old;
@@ -1,88 +0,0 @@
-- migration_history
CREATE TABLE migration_history (
version TEXT NOT NULL PRIMARY KEY,
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now'))
);
-- system_setting
CREATE TABLE system_setting (
name TEXT NOT NULL,
value TEXT NOT NULL,
description TEXT NOT NULL DEFAULT '',
UNIQUE(name)
);
-- user
CREATE TABLE user (
id INTEGER PRIMARY KEY AUTOINCREMENT,
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
row_status TEXT NOT NULL CHECK (row_status IN ('NORMAL', 'ARCHIVED')) DEFAULT 'NORMAL',
username TEXT NOT NULL UNIQUE,
role TEXT NOT NULL CHECK (role IN ('HOST', 'ADMIN', 'USER')) DEFAULT 'USER',
email TEXT NOT NULL DEFAULT '',
nickname TEXT NOT NULL DEFAULT '',
password_hash TEXT NOT NULL,
open_id TEXT NOT NULL UNIQUE
);
-- user_setting
CREATE TABLE user_setting (
user_id INTEGER NOT NULL,
key TEXT NOT NULL,
value TEXT NOT NULL,
UNIQUE(user_id, key)
);
-- memo
CREATE TABLE memo (
id INTEGER PRIMARY KEY AUTOINCREMENT,
creator_id INTEGER NOT NULL,
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
row_status TEXT NOT NULL CHECK (row_status IN ('NORMAL', 'ARCHIVED')) DEFAULT 'NORMAL',
content TEXT NOT NULL DEFAULT '',
visibility TEXT NOT NULL CHECK (visibility IN ('PUBLIC', 'PROTECTED', 'PRIVATE')) DEFAULT 'PRIVATE'
);
-- memo_organizer
CREATE TABLE memo_organizer (
id INTEGER PRIMARY KEY AUTOINCREMENT,
memo_id INTEGER NOT NULL,
user_id INTEGER NOT NULL,
pinned INTEGER NOT NULL CHECK (pinned IN (0, 1)) DEFAULT 0,
UNIQUE(memo_id, user_id)
);
-- shortcut
CREATE TABLE shortcut (
id INTEGER PRIMARY KEY AUTOINCREMENT,
creator_id INTEGER NOT NULL,
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
row_status TEXT NOT NULL CHECK (row_status IN ('NORMAL', 'ARCHIVED')) DEFAULT 'NORMAL',
title TEXT NOT NULL DEFAULT '',
payload TEXT NOT NULL DEFAULT '{}'
);
-- resource
CREATE TABLE resource (
id INTEGER PRIMARY KEY AUTOINCREMENT,
creator_id INTEGER NOT NULL,
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
filename TEXT NOT NULL DEFAULT '',
blob BLOB DEFAULT NULL,
external_link TEXT NOT NULL DEFAULT '',
type TEXT NOT NULL DEFAULT '',
size INTEGER NOT NULL DEFAULT 0
);
-- memo_resource
CREATE TABLE memo_resource (
memo_id INTEGER NOT NULL,
resource_id INTEGER NOT NULL,
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
UNIQUE(memo_id, resource_id)
);
+31 -54
View File
@@ -1,7 +1,6 @@
package db
import (
"context"
"database/sql"
"strings"
)
@@ -19,62 +18,23 @@ type MigrationHistoryFind struct {
Version *string
}
func (db *DB) FindMigrationHistory(ctx context.Context, find *MigrationHistoryFind) (*MigrationHistory, error) {
tx, err := db.Db.BeginTx(ctx, nil)
if err != nil {
return nil, err
}
defer tx.Rollback()
list, err := findMigrationHistoryList(ctx, tx, find)
if err != nil {
return nil, err
}
if len(list) == 0 {
return nil, nil
}
migrationHistory := list[0]
return migrationHistory, nil
}
func (db *DB) UpsertMigrationHistory(ctx context.Context, upsert *MigrationHistoryUpsert) (*MigrationHistory, error) {
tx, err := db.Db.BeginTx(ctx, nil)
if err != nil {
return nil, err
}
defer tx.Rollback()
migrationHistory, err := upsertMigrationHistory(ctx, tx, upsert)
if err != nil {
return nil, err
}
if err := tx.Commit(); err != nil {
return nil, err
}
return migrationHistory, nil
}
func findMigrationHistoryList(ctx context.Context, tx *sql.Tx, find *MigrationHistoryFind) ([]*MigrationHistory, error) {
func findMigrationHistoryList(db *sql.DB, find *MigrationHistoryFind) ([]*MigrationHistory, error) {
where, args := []string{"1 = 1"}, []interface{}{}
if v := find.Version; v != nil {
where, args = append(where, "version = ?"), append(args, *v)
}
query := `
rows, err := db.Query(`
SELECT
version,
created_ts
FROM
migration_history
WHERE ` + strings.Join(where, " AND ") + `
ORDER BY version DESC
`
rows, err := tx.QueryContext(ctx, query, args...)
WHERE `+strings.Join(where, " AND ")+`
ORDER BY created_ts DESC`,
args...,
)
if err != nil {
return nil, err
}
@@ -93,15 +53,24 @@ func findMigrationHistoryList(ctx context.Context, tx *sql.Tx, find *MigrationHi
migrationHistoryList = append(migrationHistoryList, &migrationHistory)
}
if err := rows.Err(); err != nil {
return nil, err
}
return migrationHistoryList, nil
}
func upsertMigrationHistory(ctx context.Context, tx *sql.Tx, upsert *MigrationHistoryUpsert) (*MigrationHistory, error) {
query := `
func findMigrationHistory(db *sql.DB, find *MigrationHistoryFind) (*MigrationHistory, error) {
list, err := findMigrationHistoryList(db, find)
if err != nil {
return nil, err
}
if len(list) == 0 {
return nil, nil
} else {
return list[0], nil
}
}
func upsertMigrationHistory(db *sql.DB, upsert *MigrationHistoryUpsert) (*MigrationHistory, error) {
row, err := db.Query(`
INSERT INTO migration_history (
version
)
@@ -110,9 +79,17 @@ func upsertMigrationHistory(ctx context.Context, tx *sql.Tx, upsert *MigrationHi
SET
version=EXCLUDED.version
RETURNING version, created_ts
`
`,
upsert.Version,
)
if err != nil {
return nil, err
}
defer row.Close()
row.Next()
var migrationHistory MigrationHistory
if err := tx.QueryRowContext(ctx, query, upsert.Version).Scan(
if err := row.Scan(
&migrationHistory.Version,
&migrationHistory.CreatedTs,
); err != nil {
+7 -35
View File
@@ -1,20 +1,18 @@
INSERT INTO
user (
`id`,
`username`,
`role`,
`email`,
`nickname`,
`role`,
`name`,
`open_id`,
`password_hash`
)
VALUES
(
101,
'demohero',
'HOST',
'demo@usememos.com',
'Demo Hero',
'HOST',
'Demo Host',
'demo_open_id',
-- raw password: secret
'$2a$14$ajq8Q7fbtFRQvXpdCq7Jcuy.Rx1h/L4J60Otx.gyNLbAYctGMJ9tK'
@@ -23,45 +21,19 @@ VALUES
INSERT INTO
user (
`id`,
`username`,
`role`,
`email`,
`nickname`,
`role`,
`name`,
`open_id`,
`password_hash`
)
VALUES
(
102,
'jack',
'USER',
'jack@usememos.com',
'USER',
'Jack',
'jack_open_id',
-- raw password: secret
'$2a$14$ajq8Q7fbtFRQvXpdCq7Jcuy.Rx1h/L4J60Otx.gyNLbAYctGMJ9tK'
);
INSERT INTO
user (
`id`,
`row_status`,
`username`,
`role`,
`email`,
`nickname`,
`open_id`,
`password_hash`
)
VALUES
(
103,
'ARCHIVED',
'bob',
'USER',
'bob@usememos.com',
'Bob',
'bob_open_id',
-- raw password: secret
'$2a$14$ajq8Q7fbtFRQvXpdCq7Jcuy.Rx1h/L4J60Otx.gyNLbAYctGMJ9tK'
);
+22 -27
View File
@@ -7,29 +7,28 @@ INSERT INTO
VALUES
(
1001,
"#Hello 👋 Welcome to memos.",
"#Hello 👋 Welcome to memos.
And here is old Jack's Page: [/u/102](/u/102)",
101
);
INSERT INTO
memo (
`id`,
`content`,
`creator_id`
)
VALUES
(
1002,
'#TODO
- [ ] Take more photos about **🌄 sunset**;
- [x] Clean the room;
- [x] Read *📖 The Little Prince*;
(👆 click to toggle status)',
101
);
INSERT INTO
memo (
`id`,
`content`,
`creator_id`,
`visibility`
)
VALUES
(
1002,
'#TODO
- [x] Take more photos about **🌄 sunset**;
- [x] Clean the room;
- [ ] Read *📖 The Little Prince*;
(👆 click to toggle status)',
101,
'PROTECTED'
);
INSERT INTO
memo (
`id`,
@@ -40,9 +39,7 @@ INSERT INTO
VALUES
(
1003,
"**Bytebase** - An open source Database CI/CD for DevOps teams.
![](https://star-history.com/bytebase.webp)
🌐 [Source code](https://github.com/bytebase/bytebase)",
'好好学习,天天向上。🤜🤛',
101,
'PUBLIC'
);
@@ -51,8 +48,7 @@ INSERT INTO
memo (
`id`,
`content`,
`creator_id`,
`visibility`
`creator_id`
)
VALUES
(
@@ -63,8 +59,7 @@ VALUES
- [ ] Watch *👦 The Boys*;
(👆 click to toggle status)
',
102,
'PROTECTED'
102
);
INSERT INTO
-13
View File
@@ -10,16 +10,3 @@ VALUES
101,
1
);
INSERT INTO
memo_organizer (
`memo_id`,
`user_id`,
`pinned`
)
VALUES
(
1003,
101,
1
);
+3 -5
View File
@@ -1,12 +1,10 @@
INSERT INTO
shortcut (
`title`,
`creator_id`,
`payload`
`creator_id`
)
VALUES
(
'inbox',
101,
'[{"type":"TYPE","value":{"operator":"IS","value":"NOT_TAGGED"},"relation":"AND"}]'
'All my memos',
101
);
-12
View File
@@ -1,12 +0,0 @@
INSERT INTO
system_setting (
`name`,
`value`,
`description`
)
VALUES
(
'allowSignUp',
'true',
''
);
+66
View File
@@ -0,0 +1,66 @@
package db
import (
"database/sql"
"strings"
)
type Table struct {
Name string
SQL string
}
//lint:ignore U1000 Ignore unused function temporarily for debugging
func findTable(db *sql.DB, tableName string) (*Table, error) {
where, args := []string{"1 = 1"}, []interface{}{}
where, args = append(where, "type = ?"), append(args, "table")
where, args = append(where, "name = ?"), append(args, tableName)
rows, err := db.Query(`
SELECT
tbl_name,
sql
FROM sqlite_schema
WHERE `+strings.Join(where, " AND "),
args...,
)
if err != nil {
return nil, err
}
defer rows.Close()
tableList := make([]*Table, 0)
for rows.Next() {
var table Table
if err := rows.Scan(
&table.Name,
&table.SQL,
); err != nil {
return nil, err
}
tableList = append(tableList, &table)
}
if err := rows.Err(); err != nil {
return nil, err
}
if len(tableList) == 0 {
return nil, nil
} else {
return tableList[0], nil
}
}
func createTable(db *sql.DB, sql string) error {
result, err := db.Exec(sql)
if err != nil {
return err
}
_, err = result.RowsAffected()
return err
}
+86 -190
View File
@@ -1,9 +1,7 @@
package store
import (
"context"
"database/sql"
"encoding/json"
"fmt"
"strings"
@@ -42,71 +40,16 @@ func (raw *memoRaw) toMemo() *api.Memo {
// Domain specific fields
Content: raw.Content,
Visibility: raw.Visibility,
DisplayTs: raw.CreatedTs,
}
}
func (s *Store) ComposeMemo(ctx context.Context, memo *api.Memo) (*api.Memo, error) {
memoOrganizer, err := s.FindMemoOrganizer(ctx, &api.MemoOrganizerFind{
MemoID: memo.ID,
UserID: memo.CreatorID,
})
if err != nil && common.ErrorCode(err) != common.NotFound {
return nil, err
} else if memoOrganizer != nil {
memo.Pinned = memoOrganizer.Pinned
}
if err = s.ComposeMemoCreator(ctx, memo); err != nil {
return nil, err
}
if err = s.ComposeMemoResourceList(ctx, memo); err != nil {
return nil, err
}
memoDisplayTsOptionKey := api.UserSettingMemoDisplayTsOptionKey
memoDisplayTsOptionSetting, err := s.FindUserSetting(ctx, &api.UserSettingFind{
UserID: memo.CreatorID,
Key: &memoDisplayTsOptionKey,
})
if err != nil {
return nil, err
}
memoDisplayTsOptionValue := "created_ts"
if memoDisplayTsOptionSetting != nil {
err = json.Unmarshal([]byte(memoDisplayTsOptionSetting.Value), &memoDisplayTsOptionValue)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal user setting memo display ts option value")
}
}
if memoDisplayTsOptionValue == "updated_ts" {
memo.DisplayTs = memo.UpdatedTs
}
return memo, nil
}
func (s *Store) CreateMemo(ctx context.Context, create *api.MemoCreate) (*api.Memo, error) {
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return nil, FormatError(err)
}
defer tx.Rollback()
memoRaw, err := createMemoRaw(ctx, tx, create)
func (s *Store) CreateMemo(create *api.MemoCreate) (*api.Memo, error) {
memoRaw, err := createMemoRaw(s.db, create)
if err != nil {
return nil, err
}
if err := tx.Commit(); err != nil {
return nil, FormatError(err)
}
if err := s.cache.UpsertCache(api.MemoCache, memoRaw.ID, memoRaw); err != nil {
return nil, err
}
memo, err := s.ComposeMemo(ctx, memoRaw.toMemo())
memo, err := s.composeMemo(memoRaw)
if err != nil {
return nil, err
}
@@ -114,27 +57,13 @@ func (s *Store) CreateMemo(ctx context.Context, create *api.MemoCreate) (*api.Me
return memo, nil
}
func (s *Store) PatchMemo(ctx context.Context, patch *api.MemoPatch) (*api.Memo, error) {
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return nil, FormatError(err)
}
defer tx.Rollback()
memoRaw, err := patchMemoRaw(ctx, tx, patch)
func (s *Store) PatchMemo(patch *api.MemoPatch) (*api.Memo, error) {
memoRaw, err := patchMemoRaw(s.db, patch)
if err != nil {
return nil, err
}
if err := tx.Commit(); err != nil {
return nil, FormatError(err)
}
if err := s.cache.UpsertCache(api.MemoCache, memoRaw.ID, memoRaw); err != nil {
return nil, err
}
memo, err := s.ComposeMemo(ctx, memoRaw.toMemo())
memo, err := s.composeMemo(memoRaw)
if err != nil {
return nil, err
}
@@ -142,21 +71,15 @@ func (s *Store) PatchMemo(ctx context.Context, patch *api.MemoPatch) (*api.Memo,
return memo, nil
}
func (s *Store) FindMemoList(ctx context.Context, find *api.MemoFind) ([]*api.Memo, error) {
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return nil, FormatError(err)
}
defer tx.Rollback()
memoRawList, err := findMemoRawList(ctx, tx, find)
func (s *Store) FindMemoList(find *api.MemoFind) ([]*api.Memo, error) {
memoRawList, err := findMemoRawList(s.db, find)
if err != nil {
return nil, err
}
list := []*api.Memo{}
for _, raw := range memoRawList {
memo, err := s.ComposeMemo(ctx, raw.toMemo())
memo, err := s.composeMemo(raw)
if err != nil {
return nil, err
}
@@ -167,29 +90,8 @@ func (s *Store) FindMemoList(ctx context.Context, find *api.MemoFind) ([]*api.Me
return list, nil
}
func (s *Store) FindMemo(ctx context.Context, find *api.MemoFind) (*api.Memo, error) {
if find.ID != nil {
memoRaw := &memoRaw{}
has, err := s.cache.FindCache(api.MemoCache, *find.ID, memoRaw)
if err != nil {
return nil, err
}
if has {
memo, err := s.ComposeMemo(ctx, memoRaw.toMemo())
if err != nil {
return nil, err
}
return memo, nil
}
}
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return nil, FormatError(err)
}
defer tx.Rollback()
list, err := findMemoRawList(ctx, tx, find)
func (s *Store) FindMemo(find *api.MemoFind) (*api.Memo, error) {
list, err := findMemoRawList(s.db, find)
if err != nil {
return nil, err
}
@@ -198,12 +100,7 @@ func (s *Store) FindMemo(ctx context.Context, find *api.MemoFind) (*api.Memo, er
return nil, &common.Error{Code: common.NotFound, Err: fmt.Errorf("not found")}
}
memoRaw := list[0]
if err := s.cache.UpsertCache(api.MemoCache, memoRaw.ID, memoRaw); err != nil {
return nil, err
}
memo, err := s.ComposeMemo(ctx, memoRaw.toMemo())
memo, err := s.composeMemo(list[0])
if err != nil {
return nil, err
}
@@ -211,43 +108,44 @@ func (s *Store) FindMemo(ctx context.Context, find *api.MemoFind) (*api.Memo, er
return memo, nil
}
func (s *Store) DeleteMemo(ctx context.Context, delete *api.MemoDelete) error {
tx, err := s.db.BeginTx(ctx, nil)
func (s *Store) DeleteMemo(delete *api.MemoDelete) error {
err := deleteMemo(s.db, delete)
if err != nil {
return FormatError(err)
}
defer tx.Rollback()
if err := deleteMemo(ctx, tx, delete); err != nil {
return FormatError(err)
}
if err := vacuum(ctx, tx); err != nil {
return err
}
if err := tx.Commit(); err != nil {
return FormatError(err)
}
s.cache.DeleteCache(api.MemoCache, delete.ID)
return nil
}
func createMemoRaw(ctx context.Context, tx *sql.Tx, create *api.MemoCreate) (*memoRaw, error) {
set := []string{"creator_id", "content", "visibility"}
args := []interface{}{create.CreatorID, create.Content, create.Visibility}
placeholder := []string{"?", "?", "?"}
func createMemoRaw(db *sql.DB, create *api.MemoCreate) (*memoRaw, error) {
set := []string{"creator_id", "content"}
placeholder := []string{"?", "?"}
args := []interface{}{create.CreatorID, create.Content}
if v := create.Visibility; v != nil {
set, placeholder, args = append(set, "visibility"), append(placeholder, "?"), append(args, *v)
}
if v := create.CreatedTs; v != nil {
set, placeholder, args = append(set, "created_ts"), append(placeholder, "?"), append(args, *v)
}
query := `
INSERT INTO memo (
` + strings.Join(set, ", ") + `
)
VALUES (` + strings.Join(placeholder, ",") + `)
RETURNING id, creator_id, created_ts, updated_ts, row_status, content, visibility
`
INSERT INTO memo (
` + strings.Join(set, ", ") + `
)
VALUES (` + strings.Join(placeholder, ",") + `)
RETURNING id, creator_id, created_ts, updated_ts, row_status, content, visibility`
row, err := db.Query(query,
args...,
)
if err != nil {
return nil, FormatError(err)
}
defer row.Close()
row.Next()
var memoRaw memoRaw
if err := tx.QueryRowContext(ctx, query, args...).Scan(
if err := row.Scan(
&memoRaw.ID,
&memoRaw.CreatorID,
&memoRaw.CreatedTs,
@@ -262,35 +160,36 @@ func createMemoRaw(ctx context.Context, tx *sql.Tx, create *api.MemoCreate) (*me
return &memoRaw, nil
}
func patchMemoRaw(ctx context.Context, tx *sql.Tx, patch *api.MemoPatch) (*memoRaw, error) {
func patchMemoRaw(db *sql.DB, patch *api.MemoPatch) (*memoRaw, error) {
set, args := []string{}, []interface{}{}
if v := patch.CreatedTs; v != nil {
set, args = append(set, "created_ts = ?"), append(args, *v)
}
if v := patch.UpdatedTs; v != nil {
set, args = append(set, "updated_ts = ?"), append(args, *v)
if v := patch.Content; v != nil {
set, args = append(set, "content = ?"), append(args, *v)
}
if v := patch.RowStatus; v != nil {
set, args = append(set, "row_status = ?"), append(args, *v)
}
if v := patch.Content; v != nil {
set, args = append(set, "content = ?"), append(args, *v)
}
if v := patch.Visibility; v != nil {
set, args = append(set, "visibility = ?"), append(args, *v)
}
args = append(args, patch.ID)
query := `
row, err := db.Query(`
UPDATE memo
SET ` + strings.Join(set, ", ") + `
SET `+strings.Join(set, ", ")+`
WHERE id = ?
RETURNING id, creator_id, created_ts, updated_ts, row_status, content, visibility
`
`, args...)
if err != nil {
return nil, FormatError(err)
}
defer row.Close()
row.Next()
var memoRaw memoRaw
if err := tx.QueryRowContext(ctx, query, args...).Scan(
if err := row.Scan(
&memoRaw.ID,
&memoRaw.CreatorID,
&memoRaw.CreatedTs,
@@ -305,7 +204,7 @@ func patchMemoRaw(ctx context.Context, tx *sql.Tx, patch *api.MemoPatch) (*memoR
return &memoRaw, nil
}
func findMemoRawList(ctx context.Context, tx *sql.Tx, find *api.MemoFind) ([]*memoRaw, error) {
func findMemoRawList(db *sql.DB, find *api.MemoFind) ([]*memoRaw, error) {
where, args := []string{"1 = 1"}, []interface{}{}
if v := find.ID; v != nil {
@@ -318,21 +217,24 @@ func findMemoRawList(ctx context.Context, tx *sql.Tx, find *api.MemoFind) ([]*me
where, args = append(where, "row_status = ?"), append(args, *v)
}
if v := find.Pinned; v != nil {
where = append(where, "id IN (SELECT memo_id FROM memo_organizer WHERE pinned = 1 AND user_id = memo.creator_id)")
where = append(where, "id in (SELECT memo_id FROM memo_organizer WHERE pinned = 1 AND user_id = memo.creator_id )")
}
if v := find.ContentSearch; v != nil {
where, args = append(where, "content LIKE ?"), append(args, "%"+*v+"%")
}
if v := find.VisibilityList; len(v) != 0 {
list := []string{}
for _, visibility := range v {
list = append(list, fmt.Sprintf("$%d", len(args)+1))
args = append(args, visibility)
}
where = append(where, fmt.Sprintf("visibility in (%s)", strings.Join(list, ",")))
if v := find.Visibility; v != nil {
where, args = append(where, "visibility = ?"), append(args, *v)
}
query := `
pagination := ""
if find.Limit > 0 {
pagination = fmt.Sprintf("%s LIMIT %d", pagination, find.Limit)
if find.Offset > 0 {
pagination = fmt.Sprintf("%s OFFSET %d", pagination, find.Offset)
}
}
rows, err := db.Query(`
SELECT
id,
creator_id,
@@ -342,10 +244,10 @@ func findMemoRawList(ctx context.Context, tx *sql.Tx, find *api.MemoFind) ([]*me
content,
visibility
FROM memo
WHERE ` + strings.Join(where, " AND ") + `
ORDER BY created_ts DESC
`
rows, err := tx.QueryContext(ctx, query, args...)
WHERE `+strings.Join(where, " AND ")+`
ORDER BY created_ts DESC`+pagination,
args...,
)
if err != nil {
return nil, FormatError(err)
}
@@ -376,38 +278,32 @@ func findMemoRawList(ctx context.Context, tx *sql.Tx, find *api.MemoFind) ([]*me
return memoRawList, nil
}
func deleteMemo(ctx context.Context, tx *sql.Tx, delete *api.MemoDelete) error {
where, args := []string{"id = ?"}, []interface{}{delete.ID}
stmt := `DELETE FROM memo WHERE ` + strings.Join(where, " AND ")
result, err := tx.ExecContext(ctx, stmt, args...)
func deleteMemo(db *sql.DB, delete *api.MemoDelete) error {
result, err := db.Exec(`DELETE FROM memo WHERE id = ?`, delete.ID)
if err != nil {
return FormatError(err)
}
rows, _ := result.RowsAffected()
if rows == 0 {
return &common.Error{Code: common.NotFound, Err: fmt.Errorf("memo not found")}
return &common.Error{Code: common.NotFound, Err: fmt.Errorf("memo ID not found: %d", delete.ID)}
}
return nil
}
func vacuumMemo(ctx context.Context, tx *sql.Tx) error {
stmt := `
DELETE FROM
memo
WHERE
creator_id NOT IN (
SELECT
id
FROM
user
)`
_, err := tx.ExecContext(ctx, stmt)
if err != nil {
return FormatError(err)
func (s *Store) composeMemo(raw *memoRaw) (*api.Memo, error) {
memo := raw.toMemo()
memoOrganizer, err := s.FindMemoOrganizer(&api.MemoOrganizerFind{
MemoID: memo.ID,
UserID: memo.CreatorID,
})
if err != nil && common.ErrorCode(err) != common.NotFound {
return nil, err
} else if memoOrganizer != nil {
memo.Pinned = memoOrganizer.Pinned
}
return nil
return memo, nil
}
+22 -100
View File
@@ -1,10 +1,8 @@
package store
import (
"context"
"database/sql"
"fmt"
"strings"
"github.com/usememos/memos/api"
"github.com/usememos/memos/common"
@@ -31,14 +29,8 @@ func (raw *memoOrganizerRaw) toMemoOrganizer() *api.MemoOrganizer {
}
}
func (s *Store) FindMemoOrganizer(ctx context.Context, find *api.MemoOrganizerFind) (*api.MemoOrganizer, error) {
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return nil, FormatError(err)
}
defer tx.Rollback()
memoOrganizerRaw, err := findMemoOrganizer(ctx, tx, find)
func (s *Store) FindMemoOrganizer(find *api.MemoOrganizerFind) (*api.MemoOrganizer, error) {
memoOrganizerRaw, err := findMemoOrganizer(s.db, find)
if err != nil {
return nil, err
}
@@ -48,44 +40,17 @@ func (s *Store) FindMemoOrganizer(ctx context.Context, find *api.MemoOrganizerFi
return memoOrganizer, nil
}
func (s *Store) UpsertMemoOrganizer(ctx context.Context, upsert *api.MemoOrganizerUpsert) error {
tx, err := s.db.BeginTx(ctx, nil)
func (s *Store) UpsertMemoOrganizer(upsert *api.MemoOrganizerUpsert) error {
err := upsertMemoOrganizer(s.db, upsert)
if err != nil {
return FormatError(err)
}
defer tx.Rollback()
if err := upsertMemoOrganizer(ctx, tx, upsert); err != nil {
return err
}
if err := tx.Commit(); err != nil {
return FormatError(err)
}
return nil
}
func (s *Store) DeleteMemoOrganizer(ctx context.Context, delete *api.MemoOrganizerDelete) error {
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return FormatError(err)
}
defer tx.Rollback()
if err := deleteMemoOrganizer(ctx, tx, delete); err != nil {
return err
}
if err := tx.Commit(); err != nil {
return FormatError(err)
}
return nil
}
func findMemoOrganizer(ctx context.Context, tx *sql.Tx, find *api.MemoOrganizerFind) (*memoOrganizerRaw, error) {
query := `
func findMemoOrganizer(db *sql.DB, find *api.MemoOrganizerFind) (*memoOrganizerRaw, error) {
row, err := db.Query(`
SELECT
id,
memo_id,
@@ -93,8 +58,7 @@ func findMemoOrganizer(ctx context.Context, tx *sql.Tx, find *api.MemoOrganizerF
pinned
FROM memo_organizer
WHERE memo_id = ? AND user_id = ?
`
row, err := tx.QueryContext(ctx, query, find.MemoID, find.UserID)
`, find.MemoID, find.UserID)
if err != nil {
return nil, FormatError(err)
}
@@ -114,15 +78,11 @@ func findMemoOrganizer(ctx context.Context, tx *sql.Tx, find *api.MemoOrganizerF
return nil, FormatError(err)
}
if err := row.Err(); err != nil {
return nil, err
}
return &memoOrganizerRaw, nil
}
func upsertMemoOrganizer(ctx context.Context, tx *sql.Tx, upsert *api.MemoOrganizerUpsert) error {
query := `
func upsertMemoOrganizer(db *sql.DB, upsert *api.MemoOrganizerUpsert) error {
row, err := db.Query(`
INSERT INTO memo_organizer (
memo_id,
user_id,
@@ -133,9 +93,20 @@ func upsertMemoOrganizer(ctx context.Context, tx *sql.Tx, upsert *api.MemoOrgani
SET
pinned = EXCLUDED.pinned
RETURNING id, memo_id, user_id, pinned
`
`,
upsert.MemoID,
upsert.UserID,
upsert.Pinned,
)
if err != nil {
return FormatError(err)
}
defer row.Close()
row.Next()
var memoOrganizer api.MemoOrganizer
if err := tx.QueryRowContext(ctx, query, upsert.MemoID, upsert.UserID, upsert.Pinned).Scan(
if err := row.Scan(
&memoOrganizer.ID,
&memoOrganizer.MemoID,
&memoOrganizer.UserID,
@@ -146,52 +117,3 @@ func upsertMemoOrganizer(ctx context.Context, tx *sql.Tx, upsert *api.MemoOrgani
return nil
}
func deleteMemoOrganizer(ctx context.Context, tx *sql.Tx, delete *api.MemoOrganizerDelete) error {
where, args := []string{}, []interface{}{}
if v := delete.MemoID; v != nil {
where, args = append(where, "memo_id = ?"), append(args, *v)
}
if v := delete.UserID; v != nil {
where, args = append(where, "user_id = ?"), append(args, *v)
}
stmt := `DELETE FROM memo_organizer WHERE ` + strings.Join(where, " AND ")
result, err := tx.ExecContext(ctx, stmt, args...)
if err != nil {
return FormatError(err)
}
rows, _ := result.RowsAffected()
if rows == 0 {
return &common.Error{Code: common.NotFound, Err: fmt.Errorf("memo organizer not found")}
}
return nil
}
func vacuumMemoOrganizer(ctx context.Context, tx *sql.Tx) error {
stmt := `
DELETE FROM
memo_organizer
WHERE
memo_id NOT IN (
SELECT
id
FROM
memo
)
OR user_id NOT IN (
SELECT
id
FROM
user
)`
_, err := tx.ExecContext(ctx, stmt)
if err != nil {
return FormatError(err)
}
return nil
}
-237
View File
@@ -1,237 +0,0 @@
package store
import (
"context"
"database/sql"
"fmt"
"strings"
"github.com/usememos/memos/api"
"github.com/usememos/memos/common"
)
// memoResourceRaw is the store model for an MemoResource.
// Fields have exactly the same meanings as MemoResource.
type memoResourceRaw struct {
MemoID int
ResourceID int
CreatedTs int64
UpdatedTs int64
}
func (raw *memoResourceRaw) toMemoResource() *api.MemoResource {
return &api.MemoResource{
MemoID: raw.MemoID,
ResourceID: raw.ResourceID,
CreatedTs: raw.CreatedTs,
UpdatedTs: raw.UpdatedTs,
}
}
func (s *Store) FindMemoResourceList(ctx context.Context, find *api.MemoResourceFind) ([]*api.MemoResource, error) {
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return nil, FormatError(err)
}
defer tx.Rollback()
memoResourceRawList, err := findMemoResourceList(ctx, tx, find)
if err != nil {
return nil, err
}
list := []*api.MemoResource{}
for _, raw := range memoResourceRawList {
memoResource := raw.toMemoResource()
list = append(list, memoResource)
}
return list, nil
}
func (s *Store) FindMemoResource(ctx context.Context, find *api.MemoResourceFind) (*api.MemoResource, error) {
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return nil, FormatError(err)
}
defer tx.Rollback()
list, err := findMemoResourceList(ctx, tx, find)
if err != nil {
return nil, err
}
if len(list) == 0 {
return nil, &common.Error{Code: common.NotFound, Err: fmt.Errorf("not found")}
}
memoResourceRaw := list[0]
return memoResourceRaw.toMemoResource(), nil
}
func (s *Store) UpsertMemoResource(ctx context.Context, upsert *api.MemoResourceUpsert) (*api.MemoResource, error) {
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return nil, FormatError(err)
}
defer tx.Rollback()
memoResourceRaw, err := upsertMemoResource(ctx, tx, upsert)
if err != nil {
return nil, err
}
if err := tx.Commit(); err != nil {
return nil, FormatError(err)
}
return memoResourceRaw.toMemoResource(), nil
}
func (s *Store) DeleteMemoResource(ctx context.Context, delete *api.MemoResourceDelete) error {
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return FormatError(err)
}
defer tx.Rollback()
if err := deleteMemoResource(ctx, tx, delete); err != nil {
return FormatError(err)
}
if err := tx.Commit(); err != nil {
return FormatError(err)
}
return nil
}
func findMemoResourceList(ctx context.Context, tx *sql.Tx, find *api.MemoResourceFind) ([]*memoResourceRaw, error) {
where, args := []string{"1 = 1"}, []interface{}{}
if v := find.MemoID; v != nil {
where, args = append(where, "memo_id = ?"), append(args, *v)
}
if v := find.ResourceID; v != nil {
where, args = append(where, "resource_id = ?"), append(args, *v)
}
query := `
SELECT
memo_id,
resource_id,
created_ts,
updated_ts
FROM memo_resource
WHERE ` + strings.Join(where, " AND ") + `
ORDER BY updated_ts DESC
`
rows, err := tx.QueryContext(ctx, query, args...)
if err != nil {
return nil, FormatError(err)
}
defer rows.Close()
memoResourceRawList := make([]*memoResourceRaw, 0)
for rows.Next() {
var memoResourceRaw memoResourceRaw
if err := rows.Scan(
&memoResourceRaw.MemoID,
&memoResourceRaw.ResourceID,
&memoResourceRaw.CreatedTs,
&memoResourceRaw.UpdatedTs,
); err != nil {
return nil, FormatError(err)
}
memoResourceRawList = append(memoResourceRawList, &memoResourceRaw)
}
if err := rows.Err(); err != nil {
return nil, err
}
return memoResourceRawList, nil
}
func upsertMemoResource(ctx context.Context, tx *sql.Tx, upsert *api.MemoResourceUpsert) (*memoResourceRaw, error) {
set := []string{"memo_id", "resource_id"}
args := []interface{}{upsert.MemoID, upsert.ResourceID}
placeholder := []string{"?", "?"}
if v := upsert.UpdatedTs; v != nil {
set, args, placeholder = append(set, "updated_ts"), append(args, v), append(placeholder, "?")
}
query := `
INSERT INTO memo_resource (
` + strings.Join(set, ", ") + `
)
VALUES (` + strings.Join(placeholder, ",") + `)
ON CONFLICT(memo_id, resource_id) DO UPDATE
SET
updated_ts = EXCLUDED.updated_ts
RETURNING memo_id, resource_id, created_ts, updated_ts
`
var memoResourceRaw memoResourceRaw
if err := tx.QueryRowContext(ctx, query, args...).Scan(
&memoResourceRaw.MemoID,
&memoResourceRaw.ResourceID,
&memoResourceRaw.CreatedTs,
&memoResourceRaw.UpdatedTs,
); err != nil {
return nil, FormatError(err)
}
return &memoResourceRaw, nil
}
func deleteMemoResource(ctx context.Context, tx *sql.Tx, delete *api.MemoResourceDelete) error {
where, args := []string{}, []interface{}{}
if v := delete.MemoID; v != nil {
where, args = append(where, "memo_id = ?"), append(args, *v)
}
if v := delete.ResourceID; v != nil {
where, args = append(where, "resource_id = ?"), append(args, *v)
}
stmt := `DELETE FROM memo_resource WHERE ` + strings.Join(where, " AND ")
result, err := tx.ExecContext(ctx, stmt, args...)
if err != nil {
return FormatError(err)
}
rows, _ := result.RowsAffected()
if rows == 0 {
return &common.Error{Code: common.NotFound, Err: fmt.Errorf("memo resource not found")}
}
return nil
}
func vacuumMemoResource(ctx context.Context, tx *sql.Tx) error {
stmt := `
DELETE FROM
memo_resource
WHERE
memo_id NOT IN (
SELECT
id
FROM
memo
)
OR resource_id NOT IN (
SELECT
id
FROM
resource
)`
_, err := tx.ExecContext(ctx, stmt)
if err != nil {
return FormatError(err)
}
return nil
}
+35 -204
View File
@@ -1,10 +1,8 @@
package store
import (
"context"
"database/sql"
"fmt"
"sort"
"strings"
"github.com/usememos/memos/api"
@@ -45,57 +43,9 @@ func (raw *resourceRaw) toResource() *api.Resource {
}
}
func (s *Store) ComposeMemoResourceList(ctx context.Context, memo *api.Memo) error {
resourceList, err := s.FindResourceList(ctx, &api.ResourceFind{
MemoID: &memo.ID,
})
func (s *Store) CreateResource(create *api.ResourceCreate) (*api.Resource, error) {
resourceRaw, err := createResource(s.db, create)
if err != nil {
return err
}
for _, resource := range resourceList {
memoResource, err := s.FindMemoResource(ctx, &api.MemoResourceFind{
MemoID: &memo.ID,
ResourceID: &resource.ID,
})
if err != nil {
return err
}
resource.CreatedTs = memoResource.CreatedTs
resource.UpdatedTs = memoResource.UpdatedTs
}
sort.Slice(resourceList, func(i, j int) bool {
if resourceList[i].CreatedTs != resourceList[j].CreatedTs {
return resourceList[i].CreatedTs < resourceList[j].CreatedTs
}
return resourceList[i].ID < resourceList[j].ID
})
memo.ResourceList = resourceList
return nil
}
func (s *Store) CreateResource(ctx context.Context, create *api.ResourceCreate) (*api.Resource, error) {
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return nil, FormatError(err)
}
defer tx.Rollback()
resourceRaw, err := createResource(ctx, tx, create)
if err != nil {
return nil, err
}
if err := tx.Commit(); err != nil {
return nil, FormatError(err)
}
if err := s.cache.UpsertCache(api.ResourceCache, resourceRaw.ID, resourceRaw); err != nil {
return nil, err
}
@@ -104,14 +54,8 @@ func (s *Store) CreateResource(ctx context.Context, create *api.ResourceCreate)
return resource, nil
}
func (s *Store) FindResourceList(ctx context.Context, find *api.ResourceFind) ([]*api.Resource, error) {
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return nil, FormatError(err)
}
defer tx.Rollback()
resourceRawList, err := findResourceList(ctx, tx, find)
func (s *Store) FindResourceList(find *api.ResourceFind) ([]*api.Resource, error) {
resourceRawList, err := findResourceList(s.db, find)
if err != nil {
return nil, err
}
@@ -124,25 +68,8 @@ func (s *Store) FindResourceList(ctx context.Context, find *api.ResourceFind) ([
return resourceList, nil
}
func (s *Store) FindResource(ctx context.Context, find *api.ResourceFind) (*api.Resource, error) {
if find.ID != nil {
resourceRaw := &resourceRaw{}
has, err := s.cache.FindCache(api.ResourceCache, *find.ID, resourceRaw)
if err != nil {
return nil, err
}
if has {
return resourceRaw.toResource(), nil
}
}
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return nil, FormatError(err)
}
defer tx.Rollback()
list, err := findResourceList(ctx, tx, find)
func (s *Store) FindResource(find *api.ResourceFind) (*api.Resource, error) {
list, err := findResourceList(s.db, find)
if err != nil {
return nil, err
}
@@ -151,67 +78,22 @@ func (s *Store) FindResource(ctx context.Context, find *api.ResourceFind) (*api.
return nil, &common.Error{Code: common.NotFound, Err: fmt.Errorf("not found")}
}
resourceRaw := list[0]
if err := s.cache.UpsertCache(api.ResourceCache, resourceRaw.ID, resourceRaw); err != nil {
return nil, err
}
resource := resourceRaw.toResource()
resource := list[0].toResource()
return resource, nil
}
func (s *Store) DeleteResource(ctx context.Context, delete *api.ResourceDelete) error {
tx, err := s.db.BeginTx(ctx, nil)
func (s *Store) DeleteResource(delete *api.ResourceDelete) error {
err := deleteResource(s.db, delete)
if err != nil {
return FormatError(err)
}
defer tx.Rollback()
if err := deleteResource(ctx, tx, delete); err != nil {
return err
}
if err := vacuum(ctx, tx); err != nil {
return err
}
if err := tx.Commit(); err != nil {
return FormatError(err)
}
s.cache.DeleteCache(api.ResourceCache, delete.ID)
return nil
}
func (s *Store) PatchResource(ctx context.Context, patch *api.ResourcePatch) (*api.Resource, error) {
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return nil, FormatError(err)
}
defer tx.Rollback()
resourceRaw, err := patchResource(ctx, tx, patch)
if err != nil {
return nil, err
}
if err := tx.Commit(); err != nil {
return nil, FormatError(err)
}
if err := s.cache.UpsertCache(api.ResourceCache, resourceRaw.ID, resourceRaw); err != nil {
return nil, err
}
resource := resourceRaw.toResource()
return resource, nil
}
func createResource(ctx context.Context, tx *sql.Tx, create *api.ResourceCreate) (*resourceRaw, error) {
query := `
func createResource(db *sql.DB, create *api.ResourceCreate) (*resourceRaw, error) {
row, err := db.Query(`
INSERT INTO resource (
filename,
blob,
@@ -220,16 +102,27 @@ func createResource(ctx context.Context, tx *sql.Tx, create *api.ResourceCreate)
creator_id
)
VALUES (?, ?, ?, ?, ?)
RETURNING id, filename, blob, type, size, creator_id, created_ts, updated_ts
`
RETURNING id, filename, blob, type, size, created_ts, updated_ts
`,
create.Filename,
create.Blob,
create.Type,
create.Size,
create.CreatorID,
)
if err != nil {
return nil, FormatError(err)
}
defer row.Close()
row.Next()
var resourceRaw resourceRaw
if err := tx.QueryRowContext(ctx, query, create.Filename, create.Blob, create.Type, create.Size, create.CreatorID).Scan(
if err := row.Scan(
&resourceRaw.ID,
&resourceRaw.Filename,
&resourceRaw.Blob,
&resourceRaw.Type,
&resourceRaw.Size,
&resourceRaw.CreatorID,
&resourceRaw.CreatedTs,
&resourceRaw.UpdatedTs,
); err != nil {
@@ -239,42 +132,7 @@ func createResource(ctx context.Context, tx *sql.Tx, create *api.ResourceCreate)
return &resourceRaw, nil
}
func patchResource(ctx context.Context, tx *sql.Tx, patch *api.ResourcePatch) (*resourceRaw, error) {
set, args := []string{}, []interface{}{}
if v := patch.UpdatedTs; v != nil {
set, args = append(set, "updated_ts = ?"), append(args, *v)
}
if v := patch.Filename; v != nil {
set, args = append(set, "filename = ?"), append(args, *v)
}
args = append(args, patch.ID)
query := `
UPDATE resource
SET ` + strings.Join(set, ", ") + `
WHERE id = ?
RETURNING id, filename, blob, type, size, creator_id, created_ts, updated_ts
`
var resourceRaw resourceRaw
if err := tx.QueryRowContext(ctx, query, args...).Scan(
&resourceRaw.ID,
&resourceRaw.Filename,
&resourceRaw.Blob,
&resourceRaw.Type,
&resourceRaw.Size,
&resourceRaw.CreatorID,
&resourceRaw.CreatedTs,
&resourceRaw.UpdatedTs,
); err != nil {
return nil, FormatError(err)
}
return &resourceRaw, nil
}
func findResourceList(ctx context.Context, tx *sql.Tx, find *api.ResourceFind) ([]*resourceRaw, error) {
func findResourceList(db *sql.DB, find *api.ResourceFind) ([]*resourceRaw, error) {
where, args := []string{"1 = 1"}, []interface{}{}
if v := find.ID; v != nil {
@@ -286,25 +144,21 @@ func findResourceList(ctx context.Context, tx *sql.Tx, find *api.ResourceFind) (
if v := find.Filename; v != nil {
where, args = append(where, "filename = ?"), append(args, *v)
}
if v := find.MemoID; v != nil {
where, args = append(where, "id in (SELECT resource_id FROM memo_resource WHERE memo_id = ?)"), append(args, *v)
}
query := `
rows, err := db.Query(`
SELECT
id,
filename,
blob,
type,
size,
creator_id,
created_ts,
updated_ts
FROM resource
WHERE ` + strings.Join(where, " AND ") + `
ORDER BY id DESC
`
rows, err := tx.QueryContext(ctx, query, args...)
WHERE `+strings.Join(where, " AND ")+`
ORDER BY created_ts DESC`,
args...,
)
if err != nil {
return nil, FormatError(err)
}
@@ -319,7 +173,6 @@ func findResourceList(ctx context.Context, tx *sql.Tx, find *api.ResourceFind) (
&resourceRaw.Blob,
&resourceRaw.Type,
&resourceRaw.Size,
&resourceRaw.CreatorID,
&resourceRaw.CreatedTs,
&resourceRaw.UpdatedTs,
); err != nil {
@@ -336,37 +189,15 @@ func findResourceList(ctx context.Context, tx *sql.Tx, find *api.ResourceFind) (
return resourceRawList, nil
}
func deleteResource(ctx context.Context, tx *sql.Tx, delete *api.ResourceDelete) error {
where, args := []string{"id = ?"}, []interface{}{delete.ID}
stmt := `DELETE FROM resource WHERE ` + strings.Join(where, " AND ")
result, err := tx.ExecContext(ctx, stmt, args...)
func deleteResource(db *sql.DB, delete *api.ResourceDelete) error {
result, err := db.Exec(`DELETE FROM resource WHERE id = ?`, delete.ID)
if err != nil {
return FormatError(err)
}
rows, _ := result.RowsAffected()
if rows == 0 {
return &common.Error{Code: common.NotFound, Err: fmt.Errorf("resource not found")}
}
return nil
}
func vacuumResource(ctx context.Context, tx *sql.Tx) error {
stmt := `
DELETE FROM
resource
WHERE
creator_id NOT IN (
SELECT
id
FROM
user
)`
_, err := tx.ExecContext(ctx, stmt)
if err != nil {
return FormatError(err)
return &common.Error{Code: common.NotFound, Err: fmt.Errorf("resource ID not found: %d", delete.ID)}
}
return nil
+44 -127
View File
@@ -1,7 +1,6 @@
package store
import (
"context"
"database/sql"
"fmt"
"strings"
@@ -40,23 +39,9 @@ func (raw *shortcutRaw) toShortcut() *api.Shortcut {
}
}
func (s *Store) CreateShortcut(ctx context.Context, create *api.ShortcutCreate) (*api.Shortcut, error) {
tx, err := s.db.BeginTx(ctx, nil)
func (s *Store) CreateShortcut(create *api.ShortcutCreate) (*api.Shortcut, error) {
shortcutRaw, err := createShortcut(s.db, create)
if err != nil {
return nil, FormatError(err)
}
defer tx.Rollback()
shortcutRaw, err := createShortcut(ctx, tx, create)
if err != nil {
return nil, err
}
if err := tx.Commit(); err != nil {
return nil, FormatError(err)
}
if err := s.cache.UpsertCache(api.ShortcutCache, shortcutRaw.ID, shortcutRaw); err != nil {
return nil, err
}
@@ -65,23 +50,9 @@ func (s *Store) CreateShortcut(ctx context.Context, create *api.ShortcutCreate)
return shortcut, nil
}
func (s *Store) PatchShortcut(ctx context.Context, patch *api.ShortcutPatch) (*api.Shortcut, error) {
tx, err := s.db.BeginTx(ctx, nil)
func (s *Store) PatchShortcut(patch *api.ShortcutPatch) (*api.Shortcut, error) {
shortcutRaw, err := patchShortcut(s.db, patch)
if err != nil {
return nil, FormatError(err)
}
defer tx.Rollback()
shortcutRaw, err := patchShortcut(ctx, tx, patch)
if err != nil {
return nil, err
}
if err := tx.Commit(); err != nil {
return nil, FormatError(err)
}
if err := s.cache.UpsertCache(api.ShortcutCache, shortcutRaw.ID, shortcutRaw); err != nil {
return nil, err
}
@@ -90,14 +61,8 @@ func (s *Store) PatchShortcut(ctx context.Context, patch *api.ShortcutPatch) (*a
return shortcut, nil
}
func (s *Store) FindShortcutList(ctx context.Context, find *api.ShortcutFind) ([]*api.Shortcut, error) {
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return nil, FormatError(err)
}
defer tx.Rollback()
shortcutRawList, err := findShortcutList(ctx, tx, find)
func (s *Store) FindShortcutList(find *api.ShortcutFind) ([]*api.Shortcut, error) {
shortcutRawList, err := findShortcutList(s.db, find)
if err != nil {
return nil, err
}
@@ -110,25 +75,8 @@ func (s *Store) FindShortcutList(ctx context.Context, find *api.ShortcutFind) ([
return list, nil
}
func (s *Store) FindShortcut(ctx context.Context, find *api.ShortcutFind) (*api.Shortcut, error) {
if find.ID != nil {
shortcutRaw := &shortcutRaw{}
has, err := s.cache.FindCache(api.ShortcutCache, *find.ID, shortcutRaw)
if err != nil {
return nil, err
}
if has {
return shortcutRaw.toShortcut(), nil
}
}
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return nil, FormatError(err)
}
defer tx.Rollback()
list, err := findShortcutList(ctx, tx, find)
func (s *Store) FindShortcut(find *api.ShortcutFind) (*api.Shortcut, error) {
list, err := findShortcutList(s.db, find)
if err != nil {
return nil, err
}
@@ -137,40 +85,22 @@ func (s *Store) FindShortcut(ctx context.Context, find *api.ShortcutFind) (*api.
return nil, &common.Error{Code: common.NotFound, Err: fmt.Errorf("not found")}
}
shortcutRaw := list[0]
if err := s.cache.UpsertCache(api.ShortcutCache, shortcutRaw.ID, shortcutRaw); err != nil {
return nil, err
}
shortcut := shortcutRaw.toShortcut()
shortcut := list[0].toShortcut()
return shortcut, nil
}
func (s *Store) DeleteShortcut(ctx context.Context, delete *api.ShortcutDelete) error {
tx, err := s.db.BeginTx(ctx, nil)
func (s *Store) DeleteShortcut(delete *api.ShortcutDelete) error {
err := deleteShortcut(s.db, delete)
if err != nil {
return FormatError(err)
}
defer tx.Rollback()
err = deleteShortcut(ctx, tx, delete)
if err != nil {
return FormatError(err)
}
if err := tx.Commit(); err != nil {
return FormatError(err)
}
s.cache.DeleteCache(api.ShortcutCache, *delete.ID)
return nil
}
func createShortcut(ctx context.Context, tx *sql.Tx, create *api.ShortcutCreate) (*shortcutRaw, error) {
query := `
func createShortcut(db *sql.DB, create *api.ShortcutCreate) (*shortcutRaw, error) {
row, err := db.Query(`
INSERT INTO shortcut (
title,
payload,
@@ -178,9 +108,19 @@ func createShortcut(ctx context.Context, tx *sql.Tx, create *api.ShortcutCreate)
)
VALUES (?, ?, ?)
RETURNING id, title, payload, creator_id, created_ts, updated_ts, row_status
`
`,
create.Title,
create.Payload,
create.CreatorID,
)
if err != nil {
return nil, FormatError(err)
}
defer row.Close()
row.Next()
var shortcutRaw shortcutRaw
if err := tx.QueryRowContext(ctx, query, create.Title, create.Payload, create.CreatorID).Scan(
if err := row.Scan(
&shortcutRaw.ID,
&shortcutRaw.Title,
&shortcutRaw.Payload,
@@ -195,12 +135,9 @@ func createShortcut(ctx context.Context, tx *sql.Tx, create *api.ShortcutCreate)
return &shortcutRaw, nil
}
func patchShortcut(ctx context.Context, tx *sql.Tx, patch *api.ShortcutPatch) (*shortcutRaw, error) {
func patchShortcut(db *sql.DB, patch *api.ShortcutPatch) (*shortcutRaw, error) {
set, args := []string{}, []interface{}{}
if v := patch.UpdatedTs; v != nil {
set, args = append(set, "updated_ts = ?"), append(args, *v)
}
if v := patch.Title; v != nil {
set, args = append(set, "title = ?"), append(args, *v)
}
@@ -213,14 +150,23 @@ func patchShortcut(ctx context.Context, tx *sql.Tx, patch *api.ShortcutPatch) (*
args = append(args, patch.ID)
query := `
row, err := db.Query(`
UPDATE shortcut
SET ` + strings.Join(set, ", ") + `
SET `+strings.Join(set, ", ")+`
WHERE id = ?
RETURNING id, title, payload, created_ts, updated_ts, row_status
`
`, args...)
if err != nil {
return nil, FormatError(err)
}
defer row.Close()
if !row.Next() {
return nil, &common.Error{Code: common.NotFound, Err: fmt.Errorf("not found")}
}
var shortcutRaw shortcutRaw
if err := tx.QueryRowContext(ctx, query, args...).Scan(
if err := row.Scan(
&shortcutRaw.ID,
&shortcutRaw.Title,
&shortcutRaw.Payload,
@@ -234,7 +180,7 @@ func patchShortcut(ctx context.Context, tx *sql.Tx, patch *api.ShortcutPatch) (*
return &shortcutRaw, nil
}
func findShortcutList(ctx context.Context, tx *sql.Tx, find *api.ShortcutFind) ([]*shortcutRaw, error) {
func findShortcutList(db *sql.DB, find *api.ShortcutFind) ([]*shortcutRaw, error) {
where, args := []string{"1 = 1"}, []interface{}{}
if v := find.ID; v != nil {
@@ -247,7 +193,7 @@ func findShortcutList(ctx context.Context, tx *sql.Tx, find *api.ShortcutFind) (
where, args = append(where, "title = ?"), append(args, *v)
}
rows, err := tx.QueryContext(ctx, `
rows, err := db.Query(`
SELECT
id,
title,
@@ -291,44 +237,15 @@ func findShortcutList(ctx context.Context, tx *sql.Tx, find *api.ShortcutFind) (
return shortcutRawList, nil
}
func deleteShortcut(ctx context.Context, tx *sql.Tx, delete *api.ShortcutDelete) error {
where, args := []string{}, []interface{}{}
if v := delete.ID; v != nil {
where, args = append(where, "id = ?"), append(args, *v)
}
if v := delete.CreatorID; v != nil {
where, args = append(where, "creator_id = ?"), append(args, *v)
}
stmt := `DELETE FROM shortcut WHERE ` + strings.Join(where, " AND ")
result, err := tx.ExecContext(ctx, stmt, args...)
func deleteShortcut(db *sql.DB, delete *api.ShortcutDelete) error {
result, err := db.Exec(`DELETE FROM shortcut WHERE id = ?`, delete.ID)
if err != nil {
return FormatError(err)
}
rows, _ := result.RowsAffected()
if rows == 0 {
return &common.Error{Code: common.NotFound, Err: fmt.Errorf("shortcut not found")}
}
return nil
}
func vacuumShortcut(ctx context.Context, tx *sql.Tx) error {
stmt := `
DELETE FROM
shortcut
WHERE
creator_id NOT IN (
SELECT
id
FROM
user
)`
_, err := tx.ExecContext(ctx, stmt)
if err != nil {
return FormatError(err)
return &common.Error{Code: common.NotFound, Err: fmt.Errorf("shortcut ID not found: %d", delete.ID)}
}
return nil
+2 -56
View File
@@ -1,75 +1,21 @@
package store
import (
"context"
"database/sql"
"github.com/usememos/memos/api"
"github.com/usememos/memos/server/profile"
)
// Store provides database access to all raw objects.
// Store provides database access to all raw objects
type Store struct {
db *sql.DB
profile *profile.Profile
cache api.CacheService
}
// New creates a new instance of Store.
// New creates a new instance of Store
func New(db *sql.DB, profile *profile.Profile) *Store {
cacheService := NewCacheService()
return &Store{
db: db,
profile: profile,
cache: cacheService,
}
}
func (s *Store) Vacuum(ctx context.Context) error {
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return FormatError(err)
}
defer tx.Rollback()
if err := vacuum(ctx, tx); err != nil {
return err
}
if err := tx.Commit(); err != nil {
return FormatError(err)
}
// Vacuum sqlite database file size after deleting resource.
if _, err := s.db.Exec("VACUUM"); err != nil {
return err
}
return nil
}
// Exec vacuum records in a transcation.
func vacuum(ctx context.Context, tx *sql.Tx) error {
if err := vacuumMemo(ctx, tx); err != nil {
return err
}
if err := vacuumResource(ctx, tx); err != nil {
return err
}
if err := vacuumShortcut(ctx, tx); err != nil {
return err
}
if err := vacuumUserSetting(ctx, tx); err != nil {
return err
}
if err := vacuumMemoOrganizer(ctx, tx); err != nil {
return err
}
if err := vacuumMemoResource(ctx, tx); err != nil {
// Prevent revive warning.
return err
}
return nil
}
-154
View File
@@ -1,154 +0,0 @@
package store
import (
"context"
"database/sql"
"fmt"
"strings"
"github.com/usememos/memos/api"
"github.com/usememos/memos/common"
)
type systemSettingRaw struct {
Name api.SystemSettingName
Value string
Description string
}
func (raw *systemSettingRaw) toSystemSetting() *api.SystemSetting {
return &api.SystemSetting{
Name: raw.Name,
Value: raw.Value,
Description: raw.Description,
}
}
func (s *Store) UpsertSystemSetting(ctx context.Context, upsert *api.SystemSettingUpsert) (*api.SystemSetting, error) {
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return nil, FormatError(err)
}
defer tx.Rollback()
systemSettingRaw, err := upsertSystemSetting(ctx, tx, upsert)
if err != nil {
return nil, err
}
if err := tx.Commit(); err != nil {
return nil, err
}
systemSetting := systemSettingRaw.toSystemSetting()
return systemSetting, nil
}
func (s *Store) FindSystemSettingList(ctx context.Context, find *api.SystemSettingFind) ([]*api.SystemSetting, error) {
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return nil, FormatError(err)
}
defer tx.Rollback()
systemSettingRawList, err := findSystemSettingList(ctx, tx, find)
if err != nil {
return nil, err
}
if err := tx.Commit(); err != nil {
return nil, err
}
list := []*api.SystemSetting{}
for _, raw := range systemSettingRawList {
list = append(list, raw.toSystemSetting())
}
return list, nil
}
func (s *Store) FindSystemSetting(ctx context.Context, find *api.SystemSettingFind) (*api.SystemSetting, error) {
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return nil, FormatError(err)
}
defer tx.Rollback()
systemSettingRawList, err := findSystemSettingList(ctx, tx, find)
if err != nil {
return nil, err
}
if len(systemSettingRawList) == 0 {
return nil, &common.Error{Code: common.NotFound, Err: fmt.Errorf("not found")}
}
return systemSettingRawList[0].toSystemSetting(), nil
}
func upsertSystemSetting(ctx context.Context, tx *sql.Tx, upsert *api.SystemSettingUpsert) (*systemSettingRaw, error) {
query := `
INSERT INTO system_setting (
name, value, description
)
VALUES (?, ?, ?)
ON CONFLICT(name) DO UPDATE
SET
value = EXCLUDED.value,
description = EXCLUDED.description
RETURNING name, value, description
`
var systemSettingRaw systemSettingRaw
if err := tx.QueryRowContext(ctx, query, upsert.Name, upsert.Value, upsert.Description).Scan(
&systemSettingRaw.Name,
&systemSettingRaw.Value,
&systemSettingRaw.Description,
); err != nil {
return nil, FormatError(err)
}
return &systemSettingRaw, nil
}
func findSystemSettingList(ctx context.Context, tx *sql.Tx, find *api.SystemSettingFind) ([]*systemSettingRaw, error) {
where, args := []string{"1 = 1"}, []interface{}{}
if v := find.Name; v != nil {
where, args = append(where, "name = ?"), append(args, v.String())
}
query := `
SELECT
name,
value,
description
FROM system_setting
WHERE ` + strings.Join(where, " AND ")
rows, err := tx.QueryContext(ctx, query, args...)
if err != nil {
return nil, FormatError(err)
}
defer rows.Close()
systemSettingRawList := make([]*systemSettingRaw, 0)
for rows.Next() {
var systemSettingRaw systemSettingRaw
if err := rows.Scan(
&systemSettingRaw.Name,
&systemSettingRaw.Value,
&systemSettingRaw.Description,
); err != nil {
return nil, FormatError(err)
}
systemSettingRawList = append(systemSettingRawList, &systemSettingRaw)
}
if err := rows.Err(); err != nil {
return nil, FormatError(err)
}
return systemSettingRawList, nil
}
+78 -190
View File
@@ -1,7 +1,6 @@
package store
import (
"context"
"database/sql"
"fmt"
"strings"
@@ -21,10 +20,9 @@ type userRaw struct {
UpdatedTs int64
// Domain specific fields
Username string
Role api.Role
Email string
Nickname string
Role api.Role
Name string
PasswordHash string
OpenID string
}
@@ -37,48 +35,17 @@ func (raw *userRaw) toUser() *api.User {
CreatedTs: raw.CreatedTs,
UpdatedTs: raw.UpdatedTs,
Username: raw.Username,
Role: raw.Role,
Email: raw.Email,
Nickname: raw.Nickname,
Role: raw.Role,
Name: raw.Name,
PasswordHash: raw.PasswordHash,
OpenID: raw.OpenID,
}
}
func (s *Store) ComposeMemoCreator(ctx context.Context, memo *api.Memo) error {
user, err := s.FindUser(ctx, &api.UserFind{
ID: &memo.CreatorID,
})
func (s *Store) CreateUser(create *api.UserCreate) (*api.User, error) {
userRaw, err := createUser(s.db, create)
if err != nil {
return err
}
user.Email = ""
user.OpenID = ""
user.UserSettingList = nil
memo.Creator = user
return nil
}
func (s *Store) CreateUser(ctx context.Context, create *api.UserCreate) (*api.User, error) {
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return nil, FormatError(err)
}
defer tx.Rollback()
userRaw, err := createUser(ctx, tx, create)
if err != nil {
return nil, err
}
if err := tx.Commit(); err != nil {
return nil, FormatError(err)
}
if err := s.cache.UpsertCache(api.UserCache, userRaw.ID, userRaw); err != nil {
return nil, err
}
@@ -87,23 +54,9 @@ func (s *Store) CreateUser(ctx context.Context, create *api.UserCreate) (*api.Us
return user, nil
}
func (s *Store) PatchUser(ctx context.Context, patch *api.UserPatch) (*api.User, error) {
tx, err := s.db.BeginTx(ctx, nil)
func (s *Store) PatchUser(patch *api.UserPatch) (*api.User, error) {
userRaw, err := patchUser(s.db, patch)
if err != nil {
return nil, FormatError(err)
}
defer tx.Rollback()
userRaw, err := patchUser(ctx, tx, patch)
if err != nil {
return nil, err
}
if err := tx.Commit(); err != nil {
return nil, FormatError(err)
}
if err := s.cache.UpsertCache(api.UserCache, userRaw.ID, userRaw); err != nil {
return nil, err
}
@@ -112,14 +65,8 @@ func (s *Store) PatchUser(ctx context.Context, patch *api.UserPatch) (*api.User,
return user, nil
}
func (s *Store) FindUserList(ctx context.Context, find *api.UserFind) ([]*api.User, error) {
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return nil, FormatError(err)
}
defer tx.Rollback()
userRawList, err := findUserList(ctx, tx, find)
func (s *Store) FindUserList(find *api.UserFind) ([]*api.User, error) {
userRawList, err := findUserList(s.db, find)
if err != nil {
return nil, err
}
@@ -132,94 +79,53 @@ func (s *Store) FindUserList(ctx context.Context, find *api.UserFind) ([]*api.Us
return list, nil
}
func (s *Store) FindUser(ctx context.Context, find *api.UserFind) (*api.User, error) {
if find.ID != nil {
userRaw := &userRaw{}
has, err := s.cache.FindCache(api.UserCache, *find.ID, userRaw)
if err != nil {
return nil, err
}
if has {
return userRaw.toUser(), nil
}
}
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return nil, FormatError(err)
}
defer tx.Rollback()
list, err := findUserList(ctx, tx, find)
func (s *Store) FindUser(find *api.UserFind) (*api.User, error) {
list, err := findUserList(s.db, find)
if err != nil {
return nil, err
}
if len(list) == 0 {
return nil, &common.Error{Code: common.NotFound, Err: fmt.Errorf("not found user with filter %+v", find)}
return nil, nil
} else if len(list) > 1 {
return nil, &common.Error{Code: common.Conflict, Err: fmt.Errorf("found %d users with filter %+v, expect 1. ", len(list), find)}
}
userRaw := list[0]
if err := s.cache.UpsertCache(api.UserCache, userRaw.ID, userRaw); err != nil {
return nil, err
}
user := userRaw.toUser()
user := list[0].toUser()
return user, nil
}
func (s *Store) DeleteUser(ctx context.Context, delete *api.UserDelete) error {
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return FormatError(err)
}
defer tx.Rollback()
if err := deleteUser(ctx, tx, delete); err != nil {
return err
}
if err := vacuum(ctx, tx); err != nil {
return err
}
if err := tx.Commit(); err != nil {
return err
}
s.cache.DeleteCache(api.UserCache, delete.ID)
return nil
}
func createUser(ctx context.Context, tx *sql.Tx, create *api.UserCreate) (*userRaw, error) {
query := `
func createUser(db *sql.DB, create *api.UserCreate) (*userRaw, error) {
row, err := db.Query(`
INSERT INTO user (
username,
role,
email,
nickname,
role,
name,
password_hash,
open_id
)
VALUES (?, ?, ?, ?, ?, ?)
RETURNING id, username, role, email, nickname, password_hash, open_id, created_ts, updated_ts, row_status
`
var userRaw userRaw
if err := tx.QueryRowContext(ctx, query,
create.Username,
create.Role,
VALUES (?, ?, ?, ?, ?)
RETURNING id, email, role, name, password_hash, open_id, created_ts, updated_ts, row_status
`,
create.Email,
create.Nickname,
create.Role,
create.Name,
create.PasswordHash,
create.OpenID,
).Scan(
)
if err != nil {
return nil, FormatError(err)
}
defer row.Close()
row.Next()
var userRaw userRaw
if err := row.Scan(
&userRaw.ID,
&userRaw.Username,
&userRaw.Role,
&userRaw.Email,
&userRaw.Nickname,
&userRaw.Role,
&userRaw.Name,
&userRaw.PasswordHash,
&userRaw.OpenID,
&userRaw.CreatedTs,
@@ -232,23 +138,17 @@ func createUser(ctx context.Context, tx *sql.Tx, create *api.UserCreate) (*userR
return &userRaw, nil
}
func patchUser(ctx context.Context, tx *sql.Tx, patch *api.UserPatch) (*userRaw, error) {
func patchUser(db *sql.DB, patch *api.UserPatch) (*userRaw, error) {
set, args := []string{}, []interface{}{}
if v := patch.UpdatedTs; v != nil {
set, args = append(set, "updated_ts = ?"), append(args, *v)
}
if v := patch.RowStatus; v != nil {
set, args = append(set, "row_status = ?"), append(args, *v)
}
if v := patch.Username; v != nil {
set, args = append(set, "username = ?"), append(args, *v)
}
if v := patch.Email; v != nil {
set, args = append(set, "email = ?"), append(args, *v)
}
if v := patch.Nickname; v != nil {
set, args = append(set, "nickname = ?"), append(args, *v)
if v := patch.Name; v != nil {
set, args = append(set, "name = ?"), append(args, *v)
}
if v := patch.PasswordHash; v != nil {
set, args = append(set, "password_hash = ?"), append(args, *v)
@@ -259,70 +159,74 @@ func patchUser(ctx context.Context, tx *sql.Tx, patch *api.UserPatch) (*userRaw,
args = append(args, patch.ID)
query := `
row, err := db.Query(`
UPDATE user
SET ` + strings.Join(set, ", ") + `
SET `+strings.Join(set, ", ")+`
WHERE id = ?
RETURNING id, username, role, email, nickname, password_hash, open_id, created_ts, updated_ts, row_status
`
var userRaw userRaw
if err := tx.QueryRowContext(ctx, query, args...).Scan(
&userRaw.ID,
&userRaw.Username,
&userRaw.Role,
&userRaw.Email,
&userRaw.Nickname,
&userRaw.PasswordHash,
&userRaw.OpenID,
&userRaw.CreatedTs,
&userRaw.UpdatedTs,
&userRaw.RowStatus,
); err != nil {
RETURNING id, email, role, name, password_hash, open_id, created_ts, updated_ts, row_status
`, args...)
if err != nil {
return nil, FormatError(err)
}
defer row.Close()
return &userRaw, nil
if row.Next() {
var userRaw userRaw
if err := row.Scan(
&userRaw.ID,
&userRaw.Email,
&userRaw.Role,
&userRaw.Name,
&userRaw.PasswordHash,
&userRaw.OpenID,
&userRaw.CreatedTs,
&userRaw.UpdatedTs,
&userRaw.RowStatus,
); err != nil {
return nil, FormatError(err)
}
return &userRaw, nil
}
return nil, &common.Error{Code: common.NotFound, Err: fmt.Errorf("user ID not found: %d", patch.ID)}
}
func findUserList(ctx context.Context, tx *sql.Tx, find *api.UserFind) ([]*userRaw, error) {
func findUserList(db *sql.DB, find *api.UserFind) ([]*userRaw, error) {
where, args := []string{"1 = 1"}, []interface{}{}
if v := find.ID; v != nil {
where, args = append(where, "id = ?"), append(args, *v)
}
if v := find.Username; v != nil {
where, args = append(where, "username = ?"), append(args, *v)
}
if v := find.Role; v != nil {
where, args = append(where, "role = ?"), append(args, *v)
}
if v := find.Email; v != nil {
where, args = append(where, "email = ?"), append(args, *v)
}
if v := find.Nickname; v != nil {
where, args = append(where, "nickname = ?"), append(args, *v)
if v := find.Name; v != nil {
where, args = append(where, "name = ?"), append(args, *v)
}
if v := find.OpenID; v != nil {
where, args = append(where, "open_id = ?"), append(args, *v)
}
query := `
rows, err := db.Query(`
SELECT
id,
username,
role,
email,
nickname,
role,
name,
password_hash,
open_id,
created_ts,
updated_ts,
row_status
FROM user
WHERE ` + strings.Join(where, " AND ") + `
ORDER BY created_ts DESC, row_status DESC
`
rows, err := tx.QueryContext(ctx, query, args...)
WHERE `+strings.Join(where, " AND ")+`
ORDER BY created_ts DESC`,
args...,
)
if err != nil {
return nil, FormatError(err)
}
@@ -333,16 +237,16 @@ func findUserList(ctx context.Context, tx *sql.Tx, find *api.UserFind) ([]*userR
var userRaw userRaw
if err := rows.Scan(
&userRaw.ID,
&userRaw.Username,
&userRaw.Role,
&userRaw.Email,
&userRaw.Nickname,
&userRaw.Role,
&userRaw.Name,
&userRaw.PasswordHash,
&userRaw.OpenID,
&userRaw.CreatedTs,
&userRaw.UpdatedTs,
&userRaw.RowStatus,
); err != nil {
fmt.Println(err)
return nil, FormatError(err)
}
@@ -355,19 +259,3 @@ func findUserList(ctx context.Context, tx *sql.Tx, find *api.UserFind) ([]*userR
return userRawList, nil
}
func deleteUser(ctx context.Context, tx *sql.Tx, delete *api.UserDelete) error {
result, err := tx.ExecContext(ctx, `
DELETE FROM user WHERE id = ?
`, delete.ID)
if err != nil {
return FormatError(err)
}
rows, _ := result.RowsAffected()
if rows == 0 {
return &common.Error{Code: common.NotFound, Err: fmt.Errorf("user not found")}
}
return nil
}
-170
View File
@@ -1,170 +0,0 @@
package store
import (
"context"
"database/sql"
"strings"
"github.com/usememos/memos/api"
)
type userSettingRaw struct {
UserID int
Key api.UserSettingKey
Value string
}
func (raw *userSettingRaw) toUserSetting() *api.UserSetting {
return &api.UserSetting{
UserID: raw.UserID,
Key: raw.Key,
Value: raw.Value,
}
}
func (s *Store) UpsertUserSetting(ctx context.Context, upsert *api.UserSettingUpsert) (*api.UserSetting, error) {
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return nil, FormatError(err)
}
defer tx.Rollback()
userSettingRaw, err := upsertUserSetting(ctx, tx, upsert)
if err != nil {
return nil, err
}
if err := tx.Commit(); err != nil {
return nil, err
}
userSetting := userSettingRaw.toUserSetting()
return userSetting, nil
}
func (s *Store) FindUserSettingList(ctx context.Context, find *api.UserSettingFind) ([]*api.UserSetting, error) {
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return nil, FormatError(err)
}
defer tx.Rollback()
userSettingRawList, err := findUserSettingList(ctx, tx, find)
if err != nil {
return nil, err
}
list := []*api.UserSetting{}
for _, raw := range userSettingRawList {
list = append(list, raw.toUserSetting())
}
return list, nil
}
func (s *Store) FindUserSetting(ctx context.Context, find *api.UserSettingFind) (*api.UserSetting, error) {
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return nil, FormatError(err)
}
defer tx.Rollback()
list, err := findUserSettingList(ctx, tx, find)
if err != nil {
return nil, err
}
if len(list) == 0 {
return nil, nil
}
userSetting := list[0].toUserSetting()
return userSetting, nil
}
func upsertUserSetting(ctx context.Context, tx *sql.Tx, upsert *api.UserSettingUpsert) (*userSettingRaw, error) {
query := `
INSERT INTO user_setting (
user_id, key, value
)
VALUES (?, ?, ?)
ON CONFLICT(user_id, key) DO UPDATE
SET
value = EXCLUDED.value
RETURNING user_id, key, value
`
var userSettingRaw userSettingRaw
if err := tx.QueryRowContext(ctx, query, upsert.UserID, upsert.Key, upsert.Value).Scan(
&userSettingRaw.UserID,
&userSettingRaw.Key,
&userSettingRaw.Value,
); err != nil {
return nil, FormatError(err)
}
return &userSettingRaw, nil
}
func findUserSettingList(ctx context.Context, tx *sql.Tx, find *api.UserSettingFind) ([]*userSettingRaw, error) {
where, args := []string{"1 = 1"}, []interface{}{}
if v := find.Key; v != nil {
where, args = append(where, "key = ?"), append(args, v.String())
}
where, args = append(where, "user_id = ?"), append(args, find.UserID)
query := `
SELECT
user_id,
key,
value
FROM user_setting
WHERE ` + strings.Join(where, " AND ")
rows, err := tx.QueryContext(ctx, query, args...)
if err != nil {
return nil, FormatError(err)
}
defer rows.Close()
userSettingRawList := make([]*userSettingRaw, 0)
for rows.Next() {
var userSettingRaw userSettingRaw
if err := rows.Scan(
&userSettingRaw.UserID,
&userSettingRaw.Key,
&userSettingRaw.Value,
); err != nil {
return nil, FormatError(err)
}
userSettingRawList = append(userSettingRawList, &userSettingRaw)
}
if err := rows.Err(); err != nil {
return nil, FormatError(err)
}
return userSettingRawList, nil
}
func vacuumUserSetting(ctx context.Context, tx *sql.Tx) error {
stmt := `
DELETE FROM
user_setting
WHERE
user_id NOT IN (
SELECT
id
FROM
user
)`
_, err := tx.ExecContext(ctx, stmt)
if err != nil {
return FormatError(err)
}
return nil
}

Some files were not shown because too many files have changed in this diff Show More