Showing preview only (1,838K chars total). Download the full file or copy to clipboard to get everything.
Repository: go-shiori/shiori
Branch: master
Commit: 585ea341aa59
Files: 328
Total size: 1.7 MB
Directory structure:
gitextract_iww9x6qw/
├── .cursorrules
├── .dockerignore
├── .github/
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug_report.md
│ │ └── feature_request.md
│ ├── dependabot.yml
│ ├── stale.yml
│ └── workflows/
│ ├── _buildx.yml
│ ├── _delete-registry-tag.yml
│ ├── _e2e.yml
│ ├── _golangci-lint.yml
│ ├── _gorelease.yml
│ ├── _mkdocs-check.yml
│ ├── _mkdocs-publish.yml
│ ├── _styles-check.yml
│ ├── _swagger-check.yml
│ ├── _test.yml
│ ├── pull_request.yml
│ ├── pull_request_closed.yml
│ ├── push.yml
│ └── version_bump.yml
├── .gitignore
├── .golangci.bck.yml
├── .golangci.yml
├── .goreleaser.yaml
├── .prettierignore
├── .prettierrc
├── CODE_OF_CONDUCT.md
├── Dockerfile
├── Dockerfile.alpine
├── Dockerfile.compose
├── Dockerfile.e2e
├── LICENSE
├── Makefile
├── Procfile
├── README.md
├── app.json
├── bun.lockb
├── codecov.yml
├── docker-compose.yaml
├── docs/
│ ├── API.md
│ ├── APIv1.md
│ ├── CLI.md
│ ├── Configuration.md
│ ├── Contribute.md
│ ├── Installation.md
│ ├── Screenshots.md
│ ├── Storage.md
│ ├── Usage.md
│ ├── assets/
│ │ └── css/
│ │ └── style.css
│ ├── faq.md
│ ├── index.md
│ ├── postman/
│ │ └── shiori.postman_collection.json
│ └── swagger/
│ ├── docs.go
│ ├── swagger.json
│ └── swagger.yaml
├── e2e/
│ ├── e2eutil/
│ │ └── containers.go
│ ├── playwright/
│ │ ├── accounts_test.go
│ │ ├── auth_test.go
│ │ ├── playwright_test.go
│ │ ├── reporter.go
│ │ └── testhelper.go
│ └── server/
│ ├── auth_test.go
│ └── basic_test.go
├── go.mod
├── go.sum
├── internal/
│ ├── assets.go
│ ├── cmd/
│ │ ├── add.go
│ │ ├── check.go
│ │ ├── delete.go
│ │ ├── export.go
│ │ ├── import.go
│ │ ├── open.go
│ │ ├── pocket.go
│ │ ├── pocket_test.go
│ │ ├── print.go
│ │ ├── root.go
│ │ ├── serve.go
│ │ ├── server.go
│ │ ├── server_test.go
│ │ ├── update.go
│ │ ├── utils.go
│ │ ├── utils_test.go
│ │ └── version.go
│ ├── config/
│ │ ├── config.go
│ │ ├── config_test.go
│ │ └── storage.go
│ ├── core/
│ │ ├── core.go
│ │ ├── download.go
│ │ ├── ebook.go
│ │ ├── ebook_test.go
│ │ ├── processing.go
│ │ ├── processing_test.go
│ │ └── url.go
│ ├── database/
│ │ ├── database.go
│ │ ├── database_tags.go
│ │ ├── database_tags_test.go
│ │ ├── database_test.go
│ │ ├── migrations/
│ │ │ ├── mysql/
│ │ │ │ ├── 0000_system_create.up.sql
│ │ │ │ ├── 0000_system_insert.up.sql
│ │ │ │ ├── 0001_initial_account.up.sql
│ │ │ │ ├── 0002_initial_bookmark.up.sql
│ │ │ │ ├── 0003_initial_tag.up.sql
│ │ │ │ ├── 0004_initial_bookmark_tag.up.sql
│ │ │ │ ├── 0005_rename_to_created_at.up.sql
│ │ │ │ ├── 0006_change_created_at_settings.up.sql
│ │ │ │ ├── 0007_add_modified_at.up.sql
│ │ │ │ ├── 0008_set_modified_at_equal_created_at.up.sql
│ │ │ │ ├── 0009_index_for_created_at.up.sql
│ │ │ │ └── 0010_index_for_modified_at.up.sql
│ │ │ ├── postgres/
│ │ │ │ ├── 0000_system.up.sql
│ │ │ │ ├── 0001_initial.up.sql
│ │ │ │ └── 0002_created_time.up.sql
│ │ │ └── sqlite/
│ │ │ ├── 0000_system.up.sql
│ │ │ ├── 0001_initial.up.sql
│ │ │ ├── 0002_denormalize_content.up.sql
│ │ │ ├── 0003_uniq_id.up.sql
│ │ │ └── 0004_created_time.up.sql
│ │ ├── migrations.go
│ │ ├── mysql.go
│ │ ├── mysql_test.go
│ │ ├── pg.go
│ │ ├── pg_test.go
│ │ ├── sqlite.go
│ │ ├── sqlite_noncgo.go
│ │ ├── sqlite_openbsd.go
│ │ └── sqlite_test.go
│ ├── dependencies/
│ │ └── dependencies.go
│ ├── domains/
│ │ ├── accounts.go
│ │ ├── accounts_test.go
│ │ ├── archiver.go
│ │ ├── auth.go
│ │ ├── auth_test.go
│ │ ├── bookmark_tags_test.go
│ │ ├── bookmarks.go
│ │ ├── bookmarks_test.go
│ │ ├── storage.go
│ │ ├── storage_test.go
│ │ ├── tags.go
│ │ └── tags_test.go
│ ├── http/
│ │ ├── handlers/
│ │ │ ├── api/
│ │ │ │ └── v1/
│ │ │ │ ├── accounts.go
│ │ │ │ ├── accounts_test.go
│ │ │ │ ├── auth.go
│ │ │ │ ├── auth_test.go
│ │ │ │ ├── bookmark_tags_test.go
│ │ │ │ ├── bookmarks.go
│ │ │ │ ├── bookmarks_test.go
│ │ │ │ ├── system.go
│ │ │ │ ├── system_test.go
│ │ │ │ ├── tags.go
│ │ │ │ └── tags_test.go
│ │ │ ├── api.go
│ │ │ ├── bookmark.go
│ │ │ ├── bookmark_test.go
│ │ │ ├── frontend.go
│ │ │ ├── frontend_test.go
│ │ │ ├── legacy.go
│ │ │ ├── legacy_test.go
│ │ │ ├── swagger.go
│ │ │ ├── swagger_test.go
│ │ │ ├── system.go
│ │ │ └── system_test.go
│ │ ├── http.go
│ │ ├── http_test.go
│ │ ├── middleware/
│ │ │ ├── auth.go
│ │ │ ├── auth_sso_proxy.go
│ │ │ ├── auth_sso_proxy_test.go
│ │ │ ├── auth_test.go
│ │ │ ├── cors.go
│ │ │ ├── cors_test.go
│ │ │ ├── logging.go
│ │ │ ├── message_response.go
│ │ │ ├── message_response_test.go
│ │ │ ├── request_id.go
│ │ │ └── request_id_test.go
│ │ ├── response/
│ │ │ ├── file.go
│ │ │ ├── file_test.go
│ │ │ ├── response.go
│ │ │ ├── response_test.go
│ │ │ ├── shortcuts.go
│ │ │ └── shortcuts_test.go
│ │ ├── server.go
│ │ ├── server_test.go
│ │ ├── templates/
│ │ │ └── templates.go
│ │ └── webcontext/
│ │ ├── auth.go
│ │ ├── auth_test.go
│ │ ├── context.go
│ │ └── keys.go
│ ├── model/
│ │ ├── account.go
│ │ ├── bookmark.go
│ │ ├── bookmark_test.go
│ │ ├── const.go
│ │ ├── database.go
│ │ ├── dependencies.go
│ │ ├── domains.go
│ │ ├── errors.go
│ │ ├── http.go
│ │ ├── legacy.go
│ │ ├── main.go
│ │ ├── ptr.go
│ │ ├── slices.go
│ │ ├── slices_test.go
│ │ ├── tag.go
│ │ ├── tag_test.go
│ │ └── validation.go
│ ├── testutil/
│ │ ├── accounts.go
│ │ ├── accounts_test.go
│ │ ├── http.go
│ │ ├── response.go
│ │ └── shiori.go
│ ├── view/
│ │ ├── 404.html
│ │ ├── archive.html
│ │ ├── assets/
│ │ │ ├── css/
│ │ │ │ ├── archive.css
│ │ │ │ └── style.css
│ │ │ ├── js/
│ │ │ │ ├── component/
│ │ │ │ │ ├── bookmark.js
│ │ │ │ │ ├── dialog.js
│ │ │ │ │ ├── eventBus.js
│ │ │ │ │ ├── login.js
│ │ │ │ │ └── pagination.js
│ │ │ │ ├── page/
│ │ │ │ │ ├── base.js
│ │ │ │ │ ├── home.js
│ │ │ │ │ └── setting.js
│ │ │ │ ├── url.js
│ │ │ │ ├── utils/
│ │ │ │ │ └── api.js
│ │ │ │ └── vue.js
│ │ │ ├── less/
│ │ │ │ ├── archive.less
│ │ │ │ ├── bookmark-item.less
│ │ │ │ ├── common.less
│ │ │ │ ├── custom-dialog.less
│ │ │ │ ├── style.less
│ │ │ │ ├── theme.less
│ │ │ │ └── variables.less
│ │ │ └── manifest.webmanifest
│ │ ├── content.html
│ │ ├── embed.go
│ │ └── index.html
│ └── webserver/
│ ├── handler-api-ext.go
│ ├── handler-api.go
│ ├── handler.go
│ ├── server.go
│ ├── utils.go
│ ├── utils_ip.go
│ └── utils_ip_test.go
├── main.go
├── mkdocs.yml
├── package.json
├── scripts/
│ ├── buildx.sh
│ ├── e2e.sh
│ ├── styles.sh
│ ├── styles_check.sh
│ ├── swagger.sh
│ ├── swagger_check.sh
│ └── test.sh
├── testdata/
│ ├── nginx.conf
│ ├── pocket-new.csv
│ └── pocket-old.csv
└── webapp/
├── .editorconfig
├── .gitattributes
├── .gitignore
├── .prettierrc.json
├── README.md
├── dist/
│ ├── assets/
│ │ ├── ArchiveView-DZOySksr.js
│ │ ├── FoldersView-B-TWh6ac.js
│ │ ├── FoldersView-tn0RQdqM.css
│ │ ├── SettingsView-BWJgD3kk.js
│ │ ├── TagsView-CmDnarVi.js
│ │ ├── index-C8c580-n.js
│ │ └── index-DoBsnBZ2.css
│ └── index.html
├── embed.go
├── env.d.ts
├── eslint.config.ts
├── index.html
├── package.json
├── src/
│ ├── App.vue
│ ├── assets/
│ │ └── main.css
│ ├── client/
│ │ ├── .openapi-generator/
│ │ │ ├── FILES
│ │ │ └── VERSION
│ │ ├── .openapi-generator-ignore
│ │ ├── apis/
│ │ │ ├── AccountsApi.ts
│ │ │ ├── AuthApi.ts
│ │ │ ├── SystemApi.ts
│ │ │ ├── TagsApi.ts
│ │ │ └── index.ts
│ │ ├── index.ts
│ │ ├── models/
│ │ │ ├── ApiV1BookmarkTagPayload.ts
│ │ │ ├── ApiV1BulkUpdateBookmarkTagsPayload.ts
│ │ │ ├── ApiV1InfoResponse.ts
│ │ │ ├── ApiV1InfoResponseVersion.ts
│ │ │ ├── ApiV1LoginRequestPayload.ts
│ │ │ ├── ApiV1LoginResponseMessage.ts
│ │ │ ├── ApiV1ReadableResponseMessage.ts
│ │ │ ├── ApiV1UpdateAccountPayload.ts
│ │ │ ├── ApiV1UpdateCachePayload.ts
│ │ │ ├── ModelAccount.ts
│ │ │ ├── ModelAccountDTO.ts
│ │ │ ├── ModelBookmarkDTO.ts
│ │ │ ├── ModelTagDTO.ts
│ │ │ ├── ModelUserConfig.ts
│ │ │ └── index.ts
│ │ └── runtime.ts
│ ├── components/
│ │ └── layout/
│ │ ├── AppLayout.vue
│ │ ├── LanguageSelector.vue
│ │ ├── Sidebar.vue
│ │ └── TopBar.vue
│ ├── locales/
│ │ ├── de.json
│ │ ├── en.json
│ │ ├── es.json
│ │ ├── fr.json
│ │ └── ja.json
│ ├── main.ts
│ ├── router/
│ │ └── index.ts
│ ├── stores/
│ │ ├── auth.ts
│ │ └── tags.ts
│ ├── utils/
│ │ └── i18n.ts
│ └── views/
│ ├── AboutView.vue
│ ├── ArchiveView.vue
│ ├── FoldersView.vue
│ ├── HomeView.vue
│ ├── LoginView.vue
│ ├── SettingsView.vue
│ └── TagsView.vue
├── tailwind.config.js
├── tsconfig.app.json
├── tsconfig.json
├── tsconfig.node.json
├── tsconfig.vitest.json
├── vite.config.ts
└── vitest.config.ts
================================================
FILE CONTENTS
================================================
================================================
FILE: .cursorrules
================================================
# Shiori Test Commands
# Run the entire test suite
make unittest
# Run SQLite database tests only
go test -timeout 10s -count=1 -tags test_sqlite_only ./internal/database
================================================
FILE: .dockerignore
================================================
dev-data*
docs
!docs/swagger
Dockerfile
*.md
/.*
================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.md
================================================
---
name: Bug report
about: Report problems with Shiori
title: One line description of the bug
labels: type:bug
assignees: ''
---
## Data
- **Shiori version**: If unknown, run `shiori version` in your terminal or check your server logs. If you don't have the command or information in your logs you are probably running an older version (1.5.4 or older).
- **Database Engine**: If unknown, SQLite is the default.
- **Operating system**:
- **CLI/Web interface/Web Extension**:
## Describe the bug / actual behavior
A clear and concise description of what the bug is.
## Expected behavior
A clear and concise description of what you expected to happen.
## To Reproduce
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
## Screenshots
If applicable, add screenshots to help explain your problem.
## Notes
Add any other context about the problem here.
================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.md
================================================
---
name: Feature request
about: PLEASE READ ISSUE
title: ''
labels: ''
assignees: ''
---
Please report feature requests in the [discussions section](https://github.com/go-shiori/shiori/discussions/categories/feature-requests).
Feature requests in issues would be likely moved on there until we plan to work on them somewhere in the future. Having them in discussions helps with the conversation and voting of future new features, as well as to keep the issues section clean and with items to address only.
Thank you.
================================================
FILE: .github/dependabot.yml
================================================
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
version: 2
updates:
# Maintain dependencies for GitHub Actions
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
groups:
all:
patterns:
- "*"
# Maintain dependencies for Golang
- package-ecosystem: "gomod"
directory: "/"
schedule:
interval: "monthly"
groups:
all:
patterns:
- "*"
================================================
FILE: .github/stale.yml
================================================
# Number of days of inactivity before an issue becomes stale
daysUntilStale: 30
# Number of days of inactivity before a stale issue is closed
daysUntilClose: -1
# Issues with these labels will never be considered stale
exemptLabels:
- tag:no-stale
- type:bug
- type:enhancement
- type:documentation
# Label to use when marking an issue as stale
staleLabel: tag:stale
# Comment to post when marking an issue as stale. Set to `false` to disable
markComment: >
This issue has been automatically marked as stale because it has not had any
activity for quite some time.
It will be closed if no further activity occurs.
Thank you for your contributions.
# Comment to post when closing a stale issue. Set to `false` to disable
closeComment: false
pulls:
daysUntilStale: 10
daysUntilClose: -1
onlyLabels:
- tag:stalebot
================================================
FILE: .github/workflows/_buildx.yml
================================================
name: "Build Docker"
on:
workflow_call:
secrets:
DOCKERHUB_USERNAME:
required: true
DOCKERHUB_TOKEN:
required: true
inputs:
tag_prefix:
description: 'The tag prefix to use'
required: false
type: string
default: ''
dockerfile:
description: 'The Dockerfile to use'
required: false
type: string
default: 'Dockerfile'
jobs:
buildx:
runs-on: ubuntu-latest
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
name: Build Docker
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
fetch-depth: 0
- uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # 5.0.0
with:
name: dist
path: ./dist
# Every pull request that goes into master
- name: Prepare master push tags
if: github.event_name == 'push' && github.ref == 'refs/heads/master'
run: |
REPO=ghcr.io/${{ github.repository }}
TAG=$(git describe --tags)
echo "tag_flags=--tag $REPO:${{ inputs.tag_prefix }}$TAG" >> $GITHUB_ENV
# New tagged version
- name: Prepare version push tags
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
run: |
REPO=ghcr.io/${{ github.repository }}
DOCKERHUB_REPO=shioriapp/shiori
TAG=$(git describe --tags)
if [[ "$TAG" == *"rc"* ]]
then
TAG2="dev"
else
TAG2="latest"
fi
echo "tag_flags=--tag $REPO:${{ inputs.tag_prefix }}$TAG --tag $REPO:${{ inputs.tag_prefix }}$TAG2 --tag $DOCKERHUB_REPO:${{ inputs.tag_prefix }}$TAG --tag $DOCKERHUB_REPO:${{ inputs.tag_prefix }}$TAG2" >> $GITHUB_ENV
# Every pull request
- name: Prepare pull request tags
if: github.event_name == 'pull_request'
run: |
echo "tag_flags=--tag ${{ github.ref }}" >> $GITHUB_ENV
REPO=ghcr.io/${{ github.repository }}
echo "tag_flags=--tag $REPO:${{ inputs.tag_prefix }}pr-${{ github.event.pull_request.number }}" >> $GITHUB_ENV
- name: Buildx
run: |
set -x
echo "${{ secrets.GITHUB_TOKEN }}" | docker login -u "${{ github.repository_owner }}" --password-stdin ghcr.io
# Login to DockerHub for versioned releases
if [[ "${{ github.event_name }}" == "push" && "${{ github.ref }}" == refs/tags/v* ]]; then
echo "${{ secrets.DOCKERHUB_TOKEN }}" | docker login -u "${{ secrets.DOCKERHUB_USERNAME }}" --password-stdin
fi
make buildx CONTAINERFILE_NAME=${{ inputs.dockerfile }} CONTAINER_BUILDX_OPTIONS="--push ${{ env.tag_flags }}"
================================================
FILE: .github/workflows/_delete-registry-tag.yml
================================================
name: Delete registry tag
on:
workflow_call:
inputs:
tag_name:
description: 'The docker tag to remove'
required: true
type: string
jobs:
purge-image-tag:
name: Delete image from ghcr.io
runs-on: ubuntu-latest
steps:
- name: Delete image tag
uses: bots-house/ghcr-delete-image-action@3827559c68cb4dcdf54d813ea9853be6d468d3a4 # v1.1.0
with:
owner: go-shiori
name: shiori
token: ${{ secrets.GITHUB_TOKEN }}
tag: ${{ inputs.tag_name }}
================================================
FILE: .github/workflows/_e2e.yml
================================================
name: "E2E Tests"
on: workflow_call
jobs:
e2e-tests:
runs-on: ubuntu-latest
name: Tests
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup go
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
with:
go-version-file: ./go.mod
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v4.1.2
- name: Install browsers
run: npx playwright install --with-deps
- run: make e2e
- name: Upload test report
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: e2e-test-report
path: e2e-report.html
if-no-files-found: ignore
================================================
FILE: .github/workflows/_golangci-lint.yml
================================================
name: "golangci-lint"
on: workflow_call
permissions:
contents: read
pull-requests: read
jobs:
golangci:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: golangci-lint
uses: golangci/golangci-lint-action@4afd733a84b1f43292c63897423277bb7f4313a9 # 8.0.0
with:
version: "v2.5.0"
# Optional: working directory, useful for monorepos
# working-directory: somedir
# Optional: golangci-lint command line arguments.
# args: --issues-exit-code=0
# Optional: show only new issues if it's a pull request. The default value is `false`.
only-new-issues: true
# Optional: if set to true then the action will use pre-installed Go.
# skip-go-installation: true
# Optional: if set to true then the action don't cache or restore ~/go/pkg.
# skip-pkg-cache: true
# Optional: if set to true then the action don't cache or restore ~/.cache/go-build.
# skip-build-cache: true
================================================
FILE: .github/workflows/_gorelease.yml
================================================
name: "Goreleaser"
on: workflow_call
permissions:
contents: write
jobs:
goreleaser:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
fetch-depth: 0
- if: ${{ !startsWith(github.ref, 'refs/tags/v') }}
run: echo "flags=--snapshot" >> $GITHUB_ENV
- name: Setup Go
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
with:
go-version-file: 'go.mod'
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@e435ccd777264be153ace6237001ef4d979d3a7a # 6.4.0
with:
distribution: goreleaser
version: v2.4.8
args: release --clean ${{ env.flags }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # 4.6.2
with:
name: dist
path: ./dist/*
================================================
FILE: .github/workflows/_mkdocs-check.yml
================================================
name: "Check mkdocs documentation"
on: workflow_call
jobs:
mkdocs-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Set up Python
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
- name: check
run: make docs
env:
MKDOCS_EXTRA_FLAGS: --strict
================================================
FILE: .github/workflows/_mkdocs-publish.yml
================================================
name: "Publish documentation"
on: workflow_call
permissions:
contents: write
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Set up Python
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
- run: make docs
env:
DOCS_COMMAND: publish
================================================
FILE: .github/workflows/_styles-check.yml
================================================
name: "styles-check"
on: workflow_call
jobs:
styles-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup Bun
uses: oven-sh/setup-bun@735343b667d3e6f658f44d0eca948eb6282f2b76 # v1
with:
bun-version: "1.0.1"
- name: Check
run: make styles-check
================================================
FILE: .github/workflows/_swagger-check.yml
================================================
name: "swagger-check"
on: workflow_call
jobs:
swagger-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup Go
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
with:
go-version-file: 'go.mod'
- name: Install dependencies
run: go install $(cat go.mod | grep swaggo/swag | cut -d " " -f 1)/cmd/swag@$(cat go.mod | grep swaggo/swag | cut -d " " -f 2)
- name: check
run: make swag-check
================================================
FILE: .github/workflows/_test.yml
================================================
name: "Unit Tests"
on:
workflow_call:
secrets:
CODECOV_TOKEN:
required: true
env:
CGO_ENABLED: 0
jobs:
test-linux:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:13.18
env:
POSTGRES_PASSWORD: shiori
POSTGRES_USER: shiori
options: >-
--health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
ports:
- 5432:5432
mariadb:
image: mariadb:10.5.27
env:
MYSQL_USER: shiori
MYSQL_PASSWORD: shiori
MYSQL_DATABASE: shiori
MYSQL_ROOT_PASSWORD: shiori
options: >-
--health-cmd="/usr/local/bin/healthcheck.sh --connect" --health-interval 10s --health-timeout 5s --health-retries 5
ports:
- 3306:3306
mysql:
image: mysql:8.0.40
env:
MYSQL_USER: shiori
MYSQL_PASSWORD: shiori
MYSQL_DATABASE: shiori
MYSQL_ROOT_PASSWORD: shiori
options: >-
--health-cmd="mysqladmin ping"
--health-interval=10s
--health-timeout=5s
--health-retries=3
ports:
- 3307:3306
name: Go unit tests (ubuntu-latest)
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup go
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
with:
go-version-file: ./go.mod
- name: Set up gotestfmt
uses: gotesttools/gotestfmt-action@8b4478c7019be847373babde9300210e7de34bfb # v2.2.0
- uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # 4.3.0
with:
path: |
~/.cache/go-build
~/go/pkg
key: golangci-lint.cache-{platform-arch}-{interval_number}-{go.mod_hash}
restore-keys: |
golangci-lint.cache-{interval_number}-
golangci-lint.cache-
- run: make unittest
env:
SHIORI_TEST_PG_URL: "postgres://shiori:shiori@localhost:5432/shiori?sslmode=disable"
SHIORI_TEST_MYSQL_URL: "shiori:shiori@(localhost:3306)/shiori"
SHIORI_TEST_MARIADB_URL: "shiori:shiori@(localhost:3307)/shiori"
CGO_ENABLED: 1 # go test -race requires cgo
- run: go build -tags osusergo,netgo -ldflags="-s -w -X main.version=$(git describe --tags) -X main.date=$(date --iso-8601=seconds)"
- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # 5.5.1
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
test-windows-macos:
strategy:
matrix:
os: [windows-latest, macos-latest]
runs-on: ${{ matrix.os }}
name: Go unit tests (${{ matrix.os }})
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup go
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
with:
go-version-file: ./go.mod
- uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # 4.3.0
with:
path: |
~/.cache/go-build
~/go/pkg
key: golangci-lint.cache-{platform-arch}-{interval_number}-{go.mod_hash}
restore-keys: |
golangci-lint.cache-{interval_number}-
golangci-lint.cache-
- run: make unittest GO_TEST_FLAGS="-tags test_sqlite_only -race -v -count=1"
env:
CGO_ENABLED: 1 # go test -race requires cgo
- run: go build -tags osusergo,netgo -ldflags="-s -w -X main.version=$(git describe --tags) -X main.date=$(date --iso-8601=seconds)"
# Please note BSD support is offered on a best-effort basis, this check is not blocking but for us to be aware of issues.
# This test also does not take into consideration the go version specified in the go.mod file and just uses the
# latest version available in the openbsd package repository.
test-bsd:
continue-on-error: true
runs-on: ubuntu-latest
strategy:
matrix:
os:
- name: openbsd
architecture: x86-64
version: "7.7"
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Test on ${{ matrix.os.name }}
uses: cross-platform-actions/action@e8a7b572196ff79ded1979dc2bb9ee67d1ddb252 # v0.29.0
with:
environment_variables: GO_VERSION
operating_system: ${{ matrix.os.name }}
architecture: ${{ matrix.os.architecture }}
version: ${{ matrix.os.version }}
shell: bash
memory: 1G
cpu_count: 1
run: |
sudo pkg_add -u
sudo pkg_add gmake git
curl -L https://go.dev/dl/go1.25.1.openbsd-amd64.tar.gz | sudo tar -C /usr/local -xzf -
export PATH=$PATH:/usr/local/go/bin
gmake unittest GO_TEST_FLAGS="-tags test_sqlite_only -v -count=1"
================================================
FILE: .github/workflows/pull_request.yml
================================================
name: 'Pull Request'
on:
pull_request:
branches:
- master
concurrency:
group: ci-tests-${{ github.ref }}-1
cancel-in-progress: true
jobs:
call-lint:
uses: ./.github/workflows/_golangci-lint.yml
call-test:
uses: ./.github/workflows/_test.yml
secrets:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
call-swagger-check:
uses: ./.github/workflows/_swagger-check.yml
call-mkdocs-check:
uses: ./.github/workflows/_mkdocs-check.yml
call-styles-check:
uses: ./.github/workflows/_styles-check.yml
call-e2e:
needs: [call-lint, call-test, call-swagger-check, call-styles-check, call-mkdocs-check]
uses: ./.github/workflows/_e2e.yml
call-gorelease:
needs: [call-e2e]
uses: ./.github/workflows/_gorelease.yml
call-buildx:
needs: call-gorelease
if: ${{ !startsWith(github.head_ref, 'dependabot/') }}
uses: ./.github/workflows/_buildx.yml
secrets:
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
call-buildx-alpine:
needs: call-gorelease
if: ${{ !startsWith(github.head_ref, 'dependabot/') }}
uses: ./.github/workflows/_buildx.yml
secrets:
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
with:
tag_prefix: alpine-
dockerfile: Dockerfile.alpine
================================================
FILE: .github/workflows/pull_request_closed.yml
================================================
name: 'Clean up Docker images from PR'
on:
pull_request:
types:
- closed
jobs:
delete-tag:
uses: ./.github/workflows/_delete-registry-tag.yml
if: github.event.pull_request.head.repo.fork == false
with:
tag_name: pr-${{ github.event.pull_request.number }}
================================================
FILE: .github/workflows/push.yml
================================================
name: 'Push'
on:
push:
branches:
- "master"
tags:
- "v*"
concurrency:
group: ci-tests-${{ github.ref }}-1
cancel-in-progress: true
jobs:
call-lint:
uses: ./.github/workflows/_golangci-lint.yml
call-test:
uses: ./.github/workflows/_test.yml
secrets:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN}}
call-e2e:
uses: ./.github/workflows/_e2e.yml
call-gorelease:
needs: [call-lint, call-test, call-e2e]
uses: ./.github/workflows/_gorelease.yml
call-buildx:
needs: call-gorelease
uses: ./.github/workflows/_buildx.yml
secrets:
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
call-buildx-alpine:
needs: call-gorelease
# only build on pull requests from the same repo for now
uses: ./.github/workflows/_buildx.yml
secrets:
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
with:
tag_prefix: alpine-
dockerfile: Dockerfile.alpine
================================================
FILE: .github/workflows/version_bump.yml
================================================
name: "Tag release"
on:
workflow_dispatch:
inputs:
version:
description: "Version to bump to, example: v1.5.2"
required: true
ref:
description: "Ref to release from"
required: true
type: string
default: master
jobs:
tag-release:
runs-on: ubuntu-latest
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
fetch-depth: 0
ref: ${{ inputs.ref }}
- name: Tag release
run: |
git config user.email "${{github.repository_owner}}@users.noreply.github.com"
git config user.name "${{github.repository_owner}}"
git tag -a ${{ github.event.inputs.version }} -m "tag release ${{ github.event.inputs.version }}"
git push --follow-tags
call-gorelease:
needs: tag-release
uses: ./.github/workflows/_gorelease.yml
call-buildx:
needs: call-gorelease
uses: ./.github/workflows/_buildx.yml
secrets:
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
call-buildx-alpine:
needs: call-gorelease
uses: ./.github/workflows/_buildx.yml
secrets:
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
with:
tag_prefix: alpine-
dockerfile: Dockerfile.alpine
call-mkdocs-publish:
needs: [call-buildx, call-buildx-alpine]
uses: ./.github/workflows/_mkdocs-publish.yml
================================================
FILE: .gitignore
================================================
# Exclude IDE
.vscode/
.idea/
# Exclude config file
*.toml
# Exclude executable file
/shiori*
# Exclude development data
/dev-data*
# Tests
/coverage.*
e2e-report.html
# Dist files
/dist
# macOS trash files
.DS_Store
# frontend
node_modules
# golang
go.work*
# workaround for buildx using podman
type=docker
# Docs
docs/.venv
build/docs
================================================
FILE: .golangci.bck.yml
================================================
# Docs: https://golangci-lint.run/usage/configuration/#config-file
run:
timeout: 5m
issues:
max-issues-per-linter: 0
max-same-issues: 0
exclude-dirs:
- internal/mocks
linters-settings:
gofmt:
simplify: true
govet:
enable-all: true
disable:
- fieldalignment
linters:
disable-all: true
enable:
- copyloopvar
- gofmt
- gosimple
# - govet # Re-enable when all shadow declarations are fixed
- ineffassign
- predeclared
- staticcheck
- unconvert
- unused
================================================
FILE: .golangci.yml
================================================
version: "2"
linters:
default: none
enable:
- copyloopvar
- ineffassign
- predeclared
- staticcheck
- unconvert
- unused
settings:
govet:
disable:
- fieldalignment
enable-all: true
exclusions:
generated: lax
presets:
- comments
- common-false-positives
- legacy
- std-error-handling
paths:
- internal/mocks
- third_party$
- builtin$
- examples$
issues:
max-issues-per-linter: 0
max-same-issues: 0
formatters:
enable:
- gofmt
settings:
gofmt:
simplify: true
exclusions:
generated: lax
paths:
- internal/mocks
- third_party$
- builtin$
- examples$
================================================
FILE: .goreleaser.yaml
================================================
version: 2
before:
hooks:
- go mod tidy
git:
ignore_tags:
- "{{ if not .IsNightly }}*-rc*{{ end }}"
builds:
- binary: shiori
env:
- CGO_ENABLED=0
- GIN_MODE=release
tags:
- netgo
- osusergo
goos:
- linux
- windows
- darwin
goarch:
- amd64
- arm
- arm64
goarm:
- "7"
ignore:
- goos: darwin
goarch: arm
- goos: windows
goarch: arm
- goos: windows
goarch: arm64
archives:
- id: shiori
name_template: >-
{{ .ProjectName }}_
{{- if eq .Os "darwin" }}Darwin{{- else if eq .Os "linux" }}Linux{{- else if eq .Os "windows" }}Windows{{- else }}{{ .Os }}{{ end }}_
{{- if eq .Arch "amd64" }}x86_64{{- else if eq .Arch "arm64" }}aarch64{{- else }}{{ .Arch }}{{ end }}_{{ .Version }}
format_overrides:
- goos: windows
format: zip
# TODO:
# upx:
# - enabled: true
# ids:
# - shiori
# goos: [linux, darwin]
# goarch: [amd64, arm, arm64]
# goarm: ["7"]
checksum:
name_template: 'checksums.txt'
snapshot:
version_template: "{{ incpatch .Version }}-next"
changelog:
sort: asc
groups:
- title: Features
regexp: '^.*?feat(\([[:word:]]+\))??!?:.+$'
order: 0
- title: "Fixes"
regexp: '^.*?fix(\([[:word:]]+\))??!?:.+$'
order: 1
- title: "Performance"
regexp: '^.*?perf(\([[:word:]]+\))??!?:.+$'
order: 2
- title: API
regexp: '^.*?api(\([[:word:]]+\))??!?:.+$'
order: 3
- title: Documentation
regexp: '^.*?docs(\([[:word:]]+\))??!?:.+$'
order: 4
- title: "Tests"
regexp: '^.*?test(\([[:word:]]+\))??!?:.+$'
order: 5
- title: CI and Delivery
regexp: '^.*?ci(\([[:word:]]+\))??!?:.+$'
order: 6
- title: Others
order: 999
filters:
exclude:
- "^deps:"
- "^chore\\(deps\\):"
release:
prerelease: auto
================================================
FILE: .prettierignore
================================================
# Ignore some files we don't want to format
*.html
*.json
*.md
*.yml
*.yaml
# Ignore build artifacts
internal/view/assets/css/
# Ignore bundled dependencies
internal/view/assets/js/dayjs.min.js
internal/view/assets/js/url.js
internal/view/assets/js/url.min.js
internal/view/assets/js/vue.js
internal/view/assets/js/vue.min.js
================================================
FILE: .prettierrc
================================================
{
"useTabs": true
}
================================================
FILE: CODE_OF_CONDUCT.md
================================================
# Community Conduct Guideline
The following conduct guideline is based on [Ruby's](https://www.ruby-lang.org/en/conduct/) code of conduct.
This document provides community guidelines for a safe, respectful, productive, and collaborative place for any person who is willing to contribute to the Shiori community. It applies to all "collaborative space", which is defined as community communications channels (such as issues, mailing lists, submitted patches, commit comments, etc.).
- Participants will be tolerant of opposing views.
- Participants must ensure that their language and actions are free of personal attacks and disparaging personal remarks.
- When interpreting the words and actions of others, participants should always assume good intentions.
- Behaviour which can be reasonably considered harassment will not be tolerated.
Instances of abusive, harassing, or otherwise unacceptable behaviour may be reported by contacting the project maintainer at deanishe@deanishe.net.
================================================
FILE: Dockerfile
================================================
# Build stage
ARG ALPINE_VERSION=3.19
FROM docker.io/library/alpine:${ALPINE_VERSION} AS builder
ARG TARGETARCH
ARG TARGETOS
ARG TARGETVARIANT
COPY dist/shiori_${TARGETOS}_${TARGETARCH}${TARGETVARIANT}/shiori /usr/bin/shiori
RUN apk add --no-cache ca-certificates tzdata && \
chmod +x /usr/bin/shiori && \
rm -rf /tmp/*
# Server image
FROM scratch
ENV PORT=8080
ENV SHIORI_DIR=/shiori
WORKDIR ${SHIORI_DIR}
LABEL org.opencontainers.image.source="https://github.com/go-shiori/shiori"
LABEL maintainer="Felipe Martin <github@fmartingr.com>"
COPY --from=builder /tmp /tmp
COPY --from=builder /usr/bin/shiori /usr/bin/shiori
COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
EXPOSE ${PORT}
ENTRYPOINT ["/usr/bin/shiori"]
CMD ["server"]
================================================
FILE: Dockerfile.alpine
================================================
ARG ALPINE_VERSION=3.19
FROM docker.io/library/alpine:${ALPINE_VERSION}
ARG TARGETARCH
ARG TARGETOS
ARG TARGETVARIANT
COPY dist/shiori_${TARGETOS}_${TARGETARCH}${TARGETVARIANT}/shiori /usr/bin/shiori
RUN apk add --no-cache ca-certificates tzdata && \
chmod +x /usr/bin/shiori && \
rm -rf /tmp/* && \
apk cache clean
ENV PORT=8080
ENV SHIORI_DIR=/shiori
WORKDIR ${SHIORI_DIR}
LABEL org.opencontainers.image.source="https://github.com/go-shiori/shiori"
LABEL maintainer="Felipe Martin <github@fmartingr.com>"
EXPOSE ${PORT}
ENTRYPOINT ["/usr/bin/shiori"]
CMD ["server"]
================================================
FILE: Dockerfile.compose
================================================
# This Dockerfile is intented for Development purposes only to use
# with the provided docker-compose.yaml file.
# Please do not run this Dockerfile in any environment that is not
# a local development scenario as this is not throroughly updated nor
# tested.
FROM docker.io/golang:1.22-alpine
WORKDIR /src/shiori
ENTRYPOINT ["go", "run", "main.go"]
CMD ["server"]
================================================
FILE: Dockerfile.e2e
================================================
ARG ALPINE_VERSION
ARG GOLANG_VERSION
FROM docker.io/golang:${GOLANG_VERSION}-alpine${ALPINE_VERSION}
WORKDIR /src/shiori
COPY . /src/shiori
RUN apk --update add git && \
go run main.go version # Using this to force go dep download by running the main command.
ENTRYPOINT ["go", "run", "main.go"]
CMD ["server"]
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2018-present Radhi Fadlillah
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: Makefile
================================================
GO ?= $(shell command -v go 2> /dev/null)
BASH ?= $(shell command -v bash 2> /dev/null)
GOLANG_VERSION := $(shell head -n 4 go.mod | tail -n 1 | cut -d " " -f 2)
# Development
SHIORI_DIR ?= dev-data
SOURCE_FILES ?=./internal/...
# Build
CGO_ENABLED ?= 0
BUILD_TIME := $(shell date -u +%Y%m%d.%H%M%S)
BUILD_HASH := $(shell git describe --tags)
BUILD_TAGS ?= osusergo,netgo,fts5
LDFLAGS += -s -w -X main.version=$(BUILD_HASH) -X main.date=$(BUILD_TIME)
# Build (container)
CONTAINER_RUNTIME := docker
CONTAINERFILE_NAME := Dockerfile
CONTAINER_ALPINE_VERSION := 3.22
BUILDX_PLATFORMS := linux/amd64,linux/arm64,linux/arm/v7
# This is used for local development only, forcing linux to create linux only images but with the arch
# of the running machine. Far from perfect but works.
LOCAL_BUILD_PLATFORM = linux/$(shell go env GOARCH)
# Testing
GO_TEST_FLAGS ?= -v -race -count=1 -tags $(BUILD_TAGS) -covermode=atomic -coverprofile=coverage.out
GOTESTFMT_FLAGS ?=
SHIORI_TEST_MYSQL_URL ?=shiori:shiori@tcp(127.0.0.1:3306)/shiori
SHIORI_TEST_MARIADB_URL ?= shiori:shiori@tcp(127.0.0.1:3307)/shiori
SHIORI_TEST_PG_URL ?= postgres://shiori:shiori@127.0.0.1:5432/shiori?sslmode=disable
# Development
GIN_MODE ?= debug
SHIORI_DEVELOPMENT ?= true
# Swagger
SWAG_VERSION := $(shell grep "swaggo/swag" go.mod | cut -d " " -f 2)
SWAGGER_DOCS_PATH ?= ./docs/swagger
# Frontend
CLEANCSS_OPTS ?= --with-rebase
# Common exports
export GOLANG_VERSION
export CONTAINER_RUNTIME
export CONTAINERFILE_NAME
export CONTAINER_ALPINE_VERSION
export BUILDX_PLATFORMS
export SOURCE_FILES
export SHIORI_TEST_MYSQL_URL
export SHIORI_TEST_MARIADB_URL
export SHIORI_TEST_PG_URL
# Help documentatin à la https://marmelab.com/blog/2016/02/29/auto-documented-makefile.html
.PHONY: help
help:
@cat Makefile | grep -v '\.PHONY' | grep -v '\help:' | grep -B1 -E '^[a-zA-Z0-9_.-]+:.*' | sed -e "s/:.*//" | sed -e "s/^## //" | grep -v '\-\-' | sed '1!G;h;$$!d' | awk 'NR%2{printf "\033[36m%-30s\033[0m",$$0;next;}1' | sort
## Cleans up build artifacts
.PHONY: clean
clean:
rm -rf dist
## Runs server for local development
.PHONY: run-server
run-server: generate
GIN_MODE=$(GIN_MODE) SHIORI_DEVELOPMENT=$(SHIORI_DEVELOPMENT) go run main.go server --log-level debug
## Runs server for local development with v2 web UI
.PHONY: run-server-v2
run-server-v2: generate
GIN_MODE=$(GIN_MODE) SHIORI_DEVELOPMENT=$(SHIORI_DEVELOPMENT) SHIORI_HTTP_SERVE_WEB_UI_V2=true go run main.go server --log-level debug
## Generate swagger docs
.PHONY: swagger
swagger:
SWAGGER_DOCS_PATH=$(SWAGGER_DOCS_PATH) $(BASH) ./scripts/swagger.sh
.PHONY: swag-check
swag-check:
REQUIRED_SWAG_VERSION=$(SWAG_VERSION) SWAGGER_DOCS_PATH=$(SWAGGER_DOCS_PATH) $(BASH) ./scripts/swagger_check.sh
.PHONY: swag-fmt
swag-fmt:
swag fmt --dir internal/http
go fmt ./internal/http/...
## Run linters
.PHONY: lint
lint: golangci-lint swag-check
## Run golangci-lint
.PHONY: golangci-lint
golangci-lint:
golangci-lint run
## Run unit tests
.PHONY: unittest
unittest:
GIN_MODE=$(GIN_MODE) GO_TEST_FLAGS="$(GO_TEST_FLAGS)" GOTESTFMT_FLAGS="$(GOTESTFMT_FLAGS)" $(BASH) -xe ./scripts/test.sh
## Run end to end tests
.PHONY: e2e
e2e:
$(BASH) -xe ./scripts/e2e.sh
## Build styles
.PHONY: styles
styles:
CLEANCSS_OPTS=$(CLEANCSS_OPTS) $(BASH) ./scripts/styles.sh
## Build styles
.PHONY: styles-check
styles-check:
CLEANCSS_OPTS=$(CLEANCSS_OPTS) $(BASH) ./scripts/styles_check.sh
## Build binary
.PHONY: build
build: clean
GIN_MODE=$(GIN_MODE) goreleaser build --clean --snapshot
## Build binary for current targer
build-local: clean
GIN_MODE=$(GIN_MODE) goreleaser build --clean --snapshot --single-target
## Build docker image using Buildx.
# used for multi-arch builds suing mainly the CI, that's why the task does not
# build the binaries using a dependency task.
.PHONY: buildx
buildx:
$(info: Make: Buildx)
@bash scripts/buildx.sh
## Build docker image for local development
buildx-local: build-local
$(info: Make: Build image locally)
CONTAINER_BUILDX_OPTIONS="-t shiori:localdev --output type=docker" BUILDX_PLATFORMS=$(LOCAL_BUILD_PLATFORM) scripts/buildx.sh
## Creates a coverage report
.PHONY: coverage
coverage:
$(GO) test $(GO_TEST_FLAGS) -coverprofile=coverage.txt $(SOURCE_FILES)
$(GO) tool cover -html=coverage.txt
## Run generate accross the project
.PHONY: generate
generate:
$(GO) generate ./...
================================================
FILE: Procfile
================================================
web: bin/shiori server -p $PORT
================================================
FILE: README.md
================================================
# Shiori
[](https://github.com/go-shiori/shiori/actions/workflows/push.yml)
[](https://goreportcard.com/report/github.com/go-shiori/shiori)
[](https://matrix.to/#/#shiori:matrix.org)
[](https://github.com/go-shiori/shiori/pkgs/container/shiori)
**Check out our latest [Announcements](https://github.com/go-shiori/shiori/discussions/categories/announcements)**
Shiori is a simple bookmarks manager written in the Go language. Intended as a simple clone of [Pocket][pocket]. You can use it as a command line application or as a web application. This application is distributed as a single binary, which means it can be installed and used easily.
![Screenshot][screenshot]
## Features
- Basic bookmarks management i.e. add, edit, delete and search.
- Import and export bookmarks from and to Netscape Bookmark file.
- Import bookmarks from Pocket.
- Simple and clean command line interface.
- Simple and pretty web interface for those who don't want to use a command line app.
- Portable, thanks to its single binary format.
- Support for sqlite3, PostgreSQL, MariaDB and MySQL as its database.
- Where possible, by default `shiori` will parse the readable content and create an offline archive of the webpage.
- [BETA] [web extension][web-extension] support for Firefox and Chrome.
![Comparison of reader mode and archive mode][mode-comparison]
## Documentation
All documentation is available in the [docs folder][documentation]. If you think there is incomplete or incorrect information, feel free to edit it by submitting a pull request.
## License
Shiori is distributed under the terms of the [MIT license][mit], which means you can use it and modify it however you want. However, if you make an enhancement for it, if possible, please send a pull request.
[documentation]: https://github.com/go-shiori/shiori/blob/master/docs/index.md
[mit]: https://choosealicense.com/licenses/mit/
[web-extension]: https://github.com/go-shiori/shiori-web-ext
[screenshot]: https://raw.githubusercontent.com/go-shiori/shiori/master/docs/assets/screenshots/cover.png
[mode-comparison]: https://raw.githubusercontent.com/go-shiori/shiori/master/docs/assets/screenshots/comparison.png
[pocket]: https://getpocket.com/
[256]: https://github.com/go-shiori/shiori/issues/256
================================================
FILE: app.json
================================================
{
"name": "Shiori",
"description": "Shiori is a simple bookmarks manager written in Go language. Intended as a simple clone of Pocket",
"keywords": [
"bookmark",
"go",
"pocket"
],
"website": "http://github.com/go-shiori/shiori",
"repository": "http://github.com/go-shiori/shiori"
}
================================================
FILE: codecov.yml
================================================
github_checks:
annotations: false
================================================
FILE: docker-compose.yaml
================================================
# Docker compose for development purposes only.
# Edit it to fit your current development needs.
services:
shiori:
build:
context: .
dockerfile: Dockerfile.compose
container_name: shiori
command:
- "server"
- "--log-level"
- "debug"
ports:
- "8080:8080"
volumes:
- "./dev-data:/srv/shiori"
- ".:/src/shiori"
- "go-mod-cache:/go/pkg/mod"
restart: unless-stopped
links:
- "postgres"
- "mariadb"
environment:
SHIORI_DIR: /srv/shiori
# SHIORI_HTTP_ROOT_PATH: /shiori/
# SHIORI_DATABASE_URL: mysql://shiori:shiori@(mariadb)/shiori?charset=utf8mb4
# SHIORI_DATABASE_URL: postgres://shiori:shiori@postgres/shiori?sslmode=disable
nginx:
image: nginx:alpine
ports:
- "8081:8081"
volumes:
- "./testdata/nginx.conf:/etc/nginx/nginx.conf:ro"
depends_on:
- shiori
postgres:
image: postgres:13.18
environment:
POSTGRES_PASSWORD: shiori
POSTGRES_USER: shiori
ports:
- "5432:5432"
mariadb:
image: mariadb:10.5.27
environment:
MYSQL_ROOT_PASSWORD: toor
MYSQL_DATABASE: shiori
MYSQL_USER: shiori
MYSQL_PASSWORD: shiori
ports:
- "3306:3306"
mysql:
image: mysql:8.0.40
environment:
MYSQL_ROOT_PASSWORD: toor
MYSQL_DATABASE: shiori
MYSQL_USER: shiori
MYSQL_PASSWORD: shiori
ports:
- "3307:3306"
volumes:
go-mod-cache:
================================================
FILE: docs/API.md
================================================
This is a brief explanation of Shiori's API. For more examples you can import this [collection](https://github.com/go-shiori/shiori/blob/master/docs/postman/shiori.postman_collection.json) in Postman.
> ⚠️ **This is the documentation for the old API. This API is deprecated and will be removed in the future. Please refer and start migrating to the [API v1](./APIv1.md) instead.**
<!-- TOC -->
- [Auth](#auth)
- [Log in](#log-in)
- [Log out](#log-out)
- [Bookmarks](#bookmarks)
- [Get bookmarks](#get-bookmarks)
- [Add bookmark](#add-bookmark)
- [Edit bookmark](#edit-bookmark)
- [Delete bookmark](#delete-bookmark)
- [Tags](#tags)
- [Get tags](#get-tags)
- [Rename tag](#rename-tag)
- [Accounts](#accounts)
- [List accounts](#list-accounts)
- [Create account](#create-account)
- [Edit account](#edit-account)
- [Delete accounts](#delete-accounts)
<!-- /TOC -->
# Auth
## Log in
Most actions require a session id. For that, you'll need to log in using your username and password.
|Request info|Value|
|-|-|
|Endpoint|`/api/login`|
|Method|`POST`|
Body:
```json
{
"username": "shiori",
"password": "gopher",
"remember": true,
"owner": true
}
```
It will return your session ID in a JSON:
```json
{
"session": "YOUR_SESSION_ID",
"account": {
"id": 1,
"username": "shiori",
"owner": true
}
}
```
## Log out
Log out of a session ID.
|Request info|Value|
|-|-|
|Endpoint|`/api/logout`|
|Method|`POST`|
|`X-Session-Id` Header|`sessionId`|
# Bookmarks
## Get bookmarks
Gets the last 30 bookmarks (last page).
|Request info|Value|
|-|-|
|Endpoint|`/api/bookmarks`|
|Method|`GET`|
|`X-Session-Id` Header|`sessionId`|
Returns:
```json
{
"bookmarks": [
{
"id": 825,
"url": "https://interesting_cool_article.com",
"title": "Cool Interesting Article",
"excerpt": "An interesting and cool article indeed!",
"author": "",
"public": 0,
"modified": "2020-12-06 00:00:00",
"imageURL": "",
"hasContent": true,
"hasArchive": true,
"tags": [
{
"id": 7,
"name": "TAG"
}
],
"createArchive": false
},
],
"maxPage": 19,
"page": 1
}
```
## Add bookmark
Add a bookmark. For some reason, Shiori ignores the provided title and excerpt, and instead fetches them automatically. Note the tag format, a regular JSON list will result in an error.
|Request info|Value|
|-|-|
|Endpoint|`/api/bookmarks`|
|Method|`POST`|
|`X-Session-Id` Header|`sessionId`|
Body:
```json
{
"url": "https://interesting_cool_article.com",
"createArchive": true,
"public": 1,
"tags": [{"name": "Interesting"}, {"name": "Cool"}],
"title": "Cool Interesting Article",
"excerpt": "An interesting and cool article indeed!"
}
```
Returns:
```json
{
"id": 827,
"url": "https://interesting_cool_article.com",
"title": "TITLE",
"excerpt": "EXCERPT",
"author": "AUTHOR",
"public": 1,
"modified": "DATE",
"html": "HTML",
"imageURL": "/bookmark/827/thumb",
"hasContent": false,
"hasArchive": true,
"tags": [
{
"name": "Interesting"
},
{
"name": "Cool"
}
],
"createArchive": true
}
```
## Edit bookmark
Modifies a bookmark, by ID.
|Request info|Value|
|-|-|
|Endpoint|`/api/bookmarks`|
|Method|`PUT`|
|`X-Session-Id` Header|`sessionId`|
Body:
```json
{
"id": 3,
"url": "https://interesting_cool_article.com",
"title": "Cool Interesting Article",
"excerpt": "An interesting and cool article indeed!",
"author": "AUTHOR",
"public": 1,
"modified": "2019-09-22 00:00:00",
"imageURL": "/bookmark/3/thumb",
"hasContent": false,
"hasArchive": false,
"tags": [],
"createArchive": false
}
```
After providing the ID, provide the modified fields. The syntax is the same as [adding](#Add-a-bookmark).
## Delete bookmark
Deletes a list of bookmarks, by their IDs.
|Request info|Value|
|-|-|
|Endpoint|`/api/bookmarks`|
|Method|`DEL`|
|`X-Session-Id` Header|`sessionId`|
Body:
```json
[1, 2, 3]
```
# Tags
## Get tags
Gets the list of tags, their IDs and the number of entries that have those tags.
|Request info|Value|
|-|-|
|Endpoint|`/api/tags`|
|Method|`GET`|
|`X-Session-Id` Header|`sessionId`|
Returns:
```json
[
{
"id": 1,
"name": "Cool",
"nBookmarks": 1
},
{
"id": 2,
"name": "Interesting",
"nBookmarks": 1
}
```
## Rename tag
Renames a tag, provided its ID.
|Request info|Value|
|-|-|
|Endpoint|`/api/tags`|
|Method|`PUT`|
|`X-Session-Id` Header|`sessionId`|
Body:
```json
{
"id": 1,
"name": "TAG_NEW_NAME"
}
```
# Accounts
## List accounts
Gets the list of all user accounts, their IDs, and whether or not they are owners.
|Request info|Value|
|-|-|
|Endpoint|`/api/accounts`|
|Method|`GET`|
|`X-Session-Id` Header|`sessionId`|
Returns:
```json
[
{
"id": 1,
"username": "shiori",
"owner": true
}
]
```
## Create account
Creates a new user.
|Request info|Value|
|-|-|
|Endpoint|`/api/accounts`|
|Method|`POST`|
|`X-Session-Id` Header|`sessionId`|
Body:
```json
{
"username": "shiori2",
"password": "gopher",
"owner": false
}
```
## Edit account
Changes an account's password or owner status.
|Request info|Value|
|-|-|
|Endpoint|`/api/accounts`|
|Method|`PUT`|
|`X-Session-Id` Header|`sessionId`|
Body:
```json
{
"username": "shiori",
"oldPassword": "gopher",
"newPassword": "gopher",
"owner": true
}
```
## Delete accounts
Deletes a list of users.
|Request info|Value|
|-|-|
|Endpoint|`/api/accounts`|
|Method|`DEL`|
|`X-Session-Id` Header|`sessionId`|
Body:
```json
["shiori", "shiori2"]
```
================================================
FILE: docs/APIv1.md
================================================
# API v1
> ℹ️ **This is the documentation for the new API. This API is still in development and though the finished endpoints should not change please consider that breaking changes may occur once its properly released. If you are looking for the current API, please [see here](./API.md).**
The new API is an ongoing effort to migrate the current API to a more modern and standard API.
The main goals of this new API are:
- Ease of development
- Use of a [modern framework](https://gin-gonic.com)
- Use of a [standard API specification](https://swagger.io/specification/)
- Self-documented API using [Swag](https://github.com/swaggo/swag)
- Improved authentication and sessions using [JWT](https://jwt.io)
- Deduplicate code between the webserver and the API by refactoring the logic into domains
- Improve testability by using interfaces and dependency injection
The current status of this new API can be checked [here](https://github.com/go-shiori/shiori/issues/640).
Since the API is self-docummented, you can check the API documentation by [running the server locally](./Contribute.md#running-the-server-locally) and visiting the [`/swagger/index.html` endpoint](http://localhost:8080/swagger/index.html).
================================================
FILE: docs/CLI.md
================================================
Content
---
<!-- TOC -->
- [Add bookmark](#add-bookmark)
<!-- /TOC -->
Add bookmark
---
To add bookmark with CLI you can use `shiori add`.
Shiori has flags to add bookmark: `shiori add --help`
```
Bookmark the specified URL
Usage:
shiori add url [flags]
Flags:
-e, --excerpt string Custom excerpt for this bookmark
-h, --help help for add
--log-archival Log the archival process
-a, --no-archival Save bookmark without creating offline archive
-o, --offline Save bookmark without fetching data from internet
-t, --tags strings Comma-separated tags for this bookmark
-i, --title string Custom title for this bookmark
Global Flags:
--log-caller logrus report caller or not
--log-level string set logrus loglevel (default "info")
--portable run shiori in portable mode
--storage-directory string path to store shiori data
```
Examples:
Add url:
`shiori add https://example.com`
Add url with tags:
`shiori add https://example.com -t "example-1,example-2"`
Add url with custom title:
`shiori add https://example.com --title "example example"`
================================================
FILE: docs/Configuration.md
================================================
# Configuration
<!-- TOC -->
- [Overall Configuration](#overall-configuration)
- [Global configuration](#global-configuration)
- [HTTP configuration variables](#http-configuration-variables)
- [Storage Configuration](#storage-configuration)
- [The data Directory](#the-data-directory)
- [Database Configuration](#database-configuration)
- [MySQL](#mysql)
- [PostgreSQL](#postgresql)
- [Reverse proxies and the webroot path](#reverse-proxies-and-the-webroot-path)
- [Nginx](#nginx)
<!-- /TOC -->
## Overall Configuration
Most configuration can be set directly using environment variables or flags. The available flags can be found by running `shiori --help`. The available environment variables are listed below.
### Global configuration
| Environment variable | Default | Required | Description |
| -------------------- | ------- | -------- | -------------------------------------- |
| `SHIORI_DEVELOPMENT` | `False` | No | Specifies if the server is in dev mode |
### HTTP configuration variables
| Environment variable | Default | Required | Description |
| ------------------------------------------ | ------- | -------- | ----------------------------------------------------- |
| `SHIORI_HTTP_ENABLED` | True | No | Enable HTTP service |
| `SHIORI_HTTP_PORT` | 8080 | No | Port number for the HTTP service |
| `SHIORI_HTTP_ADDRESS` | : | No | Address for the HTTP service |
| `SHIORI_HTTP_ROOT_PATH` | / | No | Root path for the HTTP service |
| `SHIORI_HTTP_ACCESS_LOG` | True | No | Logging accessibility for HTTP requests |
| `SHIORI_HTTP_SERVE_WEB_UI` | True | No | Serving Web UI via HTTP. Disable serves only the API. |
| `SHIORI_HTTP_SECRET_KEY` | | **Yes** | Secret key for HTTP sessions. |
| `SHIORI_HTTP_BODY_LIMIT` | 1024 | No | Limit for request body size |
| `SHIORI_HTTP_READ_TIMEOUT` | 10s | No | Maximum duration for reading the entire request |
| `SHIORI_HTTP_WRITE_TIMEOUT` | 10s | No | Maximum duration before timing out writes |
| `SHIORI_HTTP_IDLE_TIMEOUT` | 10s | No | Maximum amount of time to wait for the next request |
| `SHIORI_HTTP_DISABLE_KEEP_ALIVE` | true | No | Disable HTTP keep-alive connections |
| `SHIORI_HTTP_DISABLE_PARSE_MULTIPART_FORM` | true | No | Disable pre-parsing of multipart form |
| `SHIORI_SSO_PROXY_AUTH_ENABLED` | false | No | Enable SSO Auth Proxy Header |
| `SHIORI_SSO_PROXY_AUTH_HEADER_NAME` | Remote-User | No | List of CIDRs of trusted proxies |
| `SHIORI_SSO_PROXY_AUTH_TRUSTED` | 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, fc00::/7 | No | List of CIDRs of trusted proxies |
### Storage Configuration
The `StorageConfig` struct contains settings related to storage.
| Environment variable | Default | Required | Description |
| -------------------- | ------------- | -------- | --------------------------------------- |
| `SHIORI_DIR` | (current dir) | No | Directory where Shiori stores its data. |
#### The data Directory
Shiori is designed to work out of the box, but you can change where it stores your bookmarks if you need to.
By default, Shiori saves your bookmarks in one of the following directories:
| Platform | Directory |
| -------- | ------------------------------------------------------------ |
| Linux | `${XDG_DATA_HOME}/shiori` (default: `~/.local/share/shiori`) |
| macOS | `~/Library/Application Support/shiori` |
| Windows | `%LOCALAPPDATA%/shiori` |
If you pass the flag `--portable` to Shiori, your data will be stored in the `shiori-data` subdirectory alongside the shiori executable.
To specify a custom path, set the `SHIORI_DIR` environment variable.
### Database Configuration
| Environment variable | Default | Required | Description |
| -------------------------- | ------- | -------- | ----------------------------------------------- |
| `SHIORI_DBMS` (deprecated) | `DBMS` | No | Deprecated (Use environment variables for DBMS) |
| `SHIORI_DATABASE_URL` | `URL` | No | URL for the database (required) |
> `SHIORI_DBMS` is deprecated and will be removed in a future release. Please use `SHIORI_DATABASE_URL` instead.
Shiori uses an SQLite3 database stored in the above [data directory by default](#storage-configuration). If you prefer, you can also use MySQL or PostgreSQL database by setting the `SHIORI_DATABASE_URL` environment variable.
#### MySQL
MySQL example: `SHIORI_DATABASE_URL="mysql://username:password@(hostname:port)/database?charset=utf8mb4"`
You can find additional details in [go mysql sql driver documentation](https://github.com/go-sql-driver/mysql#dsn-data-source-name).
#### PostgreSQL
PostgreSQL example: `SHIORI_DATABASE_URL="postgres://pqgotest:password@hostname/database?sslmode=verify-full"`
You can find additional details in [go postgres sql driver documentation](https://pkg.go.dev/github.com/lib/pq).
## Reverse proxies and the webroot path
If you want to serve Shiori behind a reverse proxy, you can set the `SHIORI_HTTP_ROOT_PATH` environment variable to the path where Shiori is served, e.g. `/shiori/`.
Keep in mind this configuration wont make Shiori accessible from `/shiori` path so you need to setup your reverse proxy accordingly so it can strip the webroot path.
We provide some examples for popular reverse proxies below. Please follow your reverse proxy documentation in order to setup it properly.
### Nginx
Fox nginx, you can use the following configuration as a example. The important part **is the trailing slash in `proxy_pass` directive**:
```nginx
location /shiori/ {
proxy_pass http://localhost:8080/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
```
================================================
FILE: docs/Contribute.md
================================================
# Contribute
1. [Running the server locally](#running-the-server-locally)
2. [Updating the API documentation](#updating-the-api-documentation)
3. [Lint the code](#lint-the-code)
4. [Running tests](#running-tests)
## Running the server locally
To run the current development server with the defaults you can run the following command:
```bash
make run-server
```
## Updating the API documentation
> **ℹ️ Note:** This only applies for the Rest API documentation under the `internal/http` folder, **not** the one under `internal/webserver`.
If you make any changes to the Rest API endpoints, you need to update the swagger documentation. In order to do that, you need to have installed [swag](https://github.com/swaggo/swag).
Then, run the following command:
```bash
make swagger
```
## Updating the frontend styles
The styles that are bundled with Shiori are stored under `internal/view/assets/css/style.css` and `internal/view/assets/css/archive.css` and created from the less files under `internal/views/assets/less`.
If you want to make frontend changes you need to do that under the less files and then compile them to css. In order to do that, you need to have installed [bun](https://bun.sh).
Then, run the following command:
```bash
make styles
```
The `style.css`/`archive.css` will be updated and changes **needs to be committed** to the repository.
## Lint the code
In order to lint the code, you need to have installed [golangci-lint](https://golangci-lint.run) and [swag](https://github.com/swaggo/swag).
After that, run the following command:
```bash
make lint
```
If any errors are found please fix them before submitting your PR.
## Running tests
In order to run the test suite, you need to have running a local instance of MariaDB and PostgreSQL.
If you have docker, you can do this by running the following command with the compose file provided:
```bash
docker-compose up -d mariadb mysql postgres
```
After that, provide the environment variables for the unitest to connect to the database engines:
- `SHIORI_TEST_MYSQL_URL` for MySQL
- `SHIORI_TEST_MARIADB_URL` for MariaDB
- `SHIORI_TEST_PG_URL` for PostgreSQL
```
SHIORI_TEST_PG_URL=postgres://shiori:shiori@127.0.0.1:5432/shiori?sslmode=disable
SHIORI_TEST_MYSQL_URL=shiori:shiori@tcp(127.0.0.1:3306)/shiori
SHIORI_TEST_MARIADB_URL=shiori:shiori@tcp(127.0.0.1:3307)/shiori
```
Finally, run the tests with the following command:
```bash
make unittest
```
## Building the documentation
The documentation is built using MkDocs with the Material theme. For installation instructions, please refer to the [MkDocs installation guide](https://www.mkdocs.org/user-guide/installation/).
To preview the documentation locally while making changes, run:
```bash
mkdocs serve
```
This will start a local server at `http://127.0.0.1:8000` where you can preview your changes in real-time.
Documentation for production is generated automatically on every release and published using github pages.
## Running the server with docker
To run the development server using Docker, you can use the provided `docker-compose.yaml` file which includes both PostgreSQL and MariaDB databases:
```bash
docker compose up shiori
```
This will start the Shiori server on port 8080 with hot-reload enabled. Any changes you make to the code will automatically rebuild and restart the server.
By default, it uses SQLite mounting the local `dev-data` folder in the source code path. To use MariaDB or PostgreSQL instead, uncomment the `SHIORI_DATABASE_URL` line for the appropriate engine in the `docker-compose.yaml` file.
## Running the server using an nginx reverse proxy and a custom webroot
To test Shiori behind an nginx reverse proxy with a custom webroot (e.g., `/shiori/`), you can use the provided nginx configuration:
1. First, ensure the `SHIORI_HTTP_ROOT_PATH` environment variable is uncommented in `docker-compose.yaml`:
```yaml
SHIORI_HTTP_ROOT_PATH: /shiori/
```
2. Then start both Shiori and nginx services:
```bash
docker compose up shiori nginx
```
This will start the shiori service along with nginx. You can access Shiori using [http://localhost:8081/shiori](http://localhost:8081/shiori).
The nginx configuration in `testdata/nginx.conf` handles all the necessary configuration.
================================================
FILE: docs/Installation.md
================================================
There are several installation methods available :
<!-- TOC -->
- [Supported](#supported)
- [Using Precompiled Binary](#using-precompiled-binary)
- [Building From Source](#building-from-source)
- [Using Docker Image](#using-docker-image)
- [Community provided](#community-provided)
- [Using Kubernetes manifests](#using-kubernetes-manifests)
- [Managed Hosting](#managed-hosting)
- [PikaPods](#pikapods)
<!-- /TOC -->
## Supported
### Using Precompiled Binary
Download the latest version of `shiori` from [the release page](https://github.com/go-shiori/shiori/releases/latest), then put it in your `PATH`.
On Linux or MacOS, you can do it by adding this line to your profile file (either `$HOME/.bash_profile` or `$HOME/.profile`):
```
export PATH=$PATH:/path/to/shiori
```
Note that this will not automatically update your path for the remainder of the session. To do this, you should run:
```
source $HOME/.bash_profile
or
source $HOME/.profile
```
On Windows, you can simply set the `PATH` by using the advanced system settings.
### Building From Source
Shiori uses Go module so make sure you have version of `go >= 1.14.1` installed, then run:
```
go get -u -v github.com/go-shiori/shiori
```
### Using Docker Image
To use Docker image, you can pull the latest automated build from Docker Hub :
```
docker pull ghcr.io/go-shiori/shiori
```
If you want to build the Docker image on your own, Shiori already has its [Dockerfile](https://github.com/go-shiori/shiori/blob/master/Dockerfile), so you can build the Docker image by running :
```
docker build -t shiori .
```
## Community provided
Below this there are other ways to deploy Shiori which are not supported by the team but were provided by the community to help others have a starting point.
### Using Kubernetes manifests
If you're self-hosting with a Kubernetes cluster, here are manifest files that
you can use to deploy Shiori:
`deploy.yaml`:
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: shiori
labels:
app: shiori
spec:
replicas: 1
selector:
matchLabels:
app: shiori
template:
metadata:
labels:
app: shiori
spec:
volumes:
- name: app
hostPath:
path: /path/to/data/dir
- name: tmp
emptyDir:
medium: Memory
containers:
- name: shiori
image: ghcr.io/go-shiori/shiori:latest
command: ["/usr/bin/shiori", "serve"]
imagePullPolicy: Always
ports:
- containerPort: 8080
env:
- name: SHIORI_DIR
value: /srv/shiori
volumeMounts:
- mountPath: /srv/shiori
name: app
- mountPath: /tmp
name: tmp
```
Here we are using a local directory to persist Shiori's data. You will need
to replace `/path/to/data/dir` with the path to the directory where you want
to keep your data. We are also mounting an `EmptyDir` volume for `/tmp` so
we can successfully generate ebooks.
Since we haven't configured a database in particular,
Shiori will use SQLite. I don't think Postgres or MySQL is worth it for
such an app, but that's up to you. If you decide to use SQLite, I strongly
suggest to keep `replicas` set to 1 since SQLite usually allows at most
one writer to proceed concurrently.
To route requests to your deployment, you will need a `Service` that gets used
by an `Ingress` to handle routing. If you wand to add a path suffix or use a
sub domain, you can do so through the ingress config. We only show the bare
minimum config to get you started.
`service.yaml`
```yaml
apiVersion: v1
kind: Service
metadata:
name: shiori
spec:
type: LoadBalancer
selector:
app: shiori
ports:
- port: 8080
targetPort: 8080
```
This is using a `LoadBalancer` type which gives the most flexibility.
`ingress.yaml`:
```yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: shiori
spec:
ingressClassName: nginx
rules:
- http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: shiori
port:
number: 8080
```
## Managed Hosting
If you don't manage your own server, the below providers will host Shiori for you. None are endorsed by or affiliated with the team. Support is provided by the providers.
### CloudBreak
[CloudBreak](https://cloudbreak.app/products/shiori?utm_medium=referral&utm_source=shiori-docs&rby=shiori-docs) offers Shiori hosting from $12/year ($1/month). Get $3 off with coupon `SHIORI`.
<a href="https://cloudbreak.app/products/shiori?utm_medium=referral&utm_source=shiori-docs&rby=shiori-docs">
<img src="https://cloudbreak.app/external/subscribe-button.png" alt="Subscribe on CloudBreak" width="149" height="64">
</a>
### PikaPods
[PikaPods](https://www.pikapods.com/) offers Shiori hosting from $1.20/month with $5 free welcome credit. EU and US regions available. Updates are applied weekly and user data backed up daily.
[](https://www.pikapods.com/pods?run=shiori)
================================================
FILE: docs/Screenshots.md
================================================
# Desktop Screenshots
## Login Screen
=== "Light Theme"
[](./assets/screenshots/01-login.png)
=== "Dark Theme"
[](./assets/screenshots/05-dark-login.png)
## Grid Mode
=== "Light Theme"
[](./assets/screenshots/02-home.png)
=== "Dark Theme"
[](./assets/screenshots/06-dark-home.png)
## List Mode
=== "Light Theme"
[](./assets/screenshots/03-home-list.png)
=== "Dark Theme"
[](./assets/screenshots/07-dark-home-list.png)
## Options Page
=== "Light Theme"
[](./assets/screenshots/04-options.png)
=== "Dark Theme"
[](./assets/screenshots/08-dark-options.png)
# Mobile Screenshots
## Login Screen
=== "Light Theme"
[](./assets/screenshots/09-mobile-login.png)
=== "Dark Theme"
[](./assets/screenshots/13-mobile-dark-login.png)
## Grid Mode
=== "Light Theme"
[](./assets/screenshots/10-mobile-home.png)
=== "Dark Theme"
[](./assets/screenshots/14-mobile-dark-home.png)
## List Mode
=== "Light Theme"
[](./assets/screenshots/11-mobile-home-list.png)
=== "Dark Theme"
[](./assets/screenshots/15-mobile-dark-home-list.png)
## Options Page
=== "Light Theme"
[](./assets/screenshots/12-mobile-options.png)
=== "Dark Theme"
[](./assets/screenshots/16-mobile-dark-options.png)
================================================
FILE: docs/Storage.md
================================================
# Storage
Shiori requires a folder to store several pieces of data, such as the bookmark archives, thumbnails, ebooks, and others. If the database engine used is sqlite, then the database file will also be stored in this folder.
You can specify the storage folder by using `--storage-dir` or `--portable` flags when running Shiori.
If none specified, Shiori will try to find the correct app folder for your OS.
For example:
- In Windows, Shiori will use `%APPDATA%`.
- In Linux, it will use `$XDG_CONFIG_HOME` or `$HOME/.local/share` if `$XDG_CONFIG_HOME` is not set.
- In macOS, it will use `$HOME/Library/Application Support`.
> For more and up to date information about app folder discovery check [muesli/go-app-paths](https://github.com/muesli/go-app-paths)
================================================
FILE: docs/Usage.md
================================================
Before using `shiori`, make sure it has been installed on your system. By default, `shiori` will store its data in directory `$HOME/.local/share/shiori`. If you want to set the data directory to another location, you can set the environment variable `SHIORI_DIR` (`ENV_SHIORI_DIR` when you are before `1.5.0`) to your desired path.
<!-- TOC -->
- [Running Docker Container](#running-docker-container)
- [Using Command Line Interface](#using-command-line-interface)
- [Search syntax](#search-syntax)
- [Using Web Interface](#using-web-interface)
- [Community contributions](#community-contributions)
- [Improved import from Pocket](#improved-import-from-pocket)
- [Import from Wallabag](#import-from-wallabag)
- [Add URL to Shiori from Android](#add-url-to-shiori-from-android)
<!-- /TOC -->
## Running Docker Container
> If you are not using `shiori` from Docker image, you can skip this section.
After building or pulling the image, you will be able to start a container from it. To preserve the data, you need to bind the directory for storing database, thumbnails and archive. In this example we're binding the data directory to our current working directory :
```
docker run -d --rm --name shiori -p 8080:8080 -v $(pwd):/shiori ghcr.io/go-shiori/shiori
```
The above command will :
- Creates a new container from image `ghcr.io/go-shiori/shiori`.
- Set the container name to `shiori` (option `--name`).
- Bind the host current working directory to `/shiori` inside container (option `-v`).
- Expose port `8080` in container to port `8080` in host machine (option `-p`).
- Run the container in background (option `-d`).
- Automatically remove the container when it stopped (option `--rm`).
After you've run the container in background, you can access console of the container:
> In order to be able to access the container and execute commands you need to use the `alpine-` prefixed images.
```
docker exec -it shiori ash
```
Now you can use `shiori` like normal. If you've finished, you can stop and remove the container by running :
```
docker stop shiori
```
## Using Command Line Interface
Shiori is composed by several subcommands. To see the documentation, run `shiori -h` :
```
Simple command-line bookmark manager built with Go
Usage:
shiori [command]
Available Commands:
add Bookmark the specified URL
check Find bookmarked sites that no longer exists on the internet
delete Delete the saved bookmarks
export Export bookmarks into HTML file in Netscape Bookmark format
help Help about any command
import Import bookmarks from HTML file in Netscape Bookmark format
open Open the saved bookmarks
pocket Import bookmarks from Pocket's exported HTML file
print Print the saved bookmarks
server Run the Shiori webserver
update Update the saved bookmarks
version Output the shiori version
Flags:
-h, --help help for shiori
--portable run shiori in portable mode
Use "shiori [command] --help" for more information about a command.
```
### Search syntax
With the `print` command line interface, you can use `-s` flag to submit keywords that will be searched either in url, title, excerpts or cached content.
You may also use `-t` flag to include tags and `-e` flag to exclude tags.
## Using Web Interface
To access web interface run `shiori server` or start Docker container following tutorial above. If you want to use a different port instead of 8080, you can simply run `shiori server -p <portnumber>`. Once started you can access the web interface in `http://localhost:8080` or `http://localhost:<portnumber>` if you customized it. You will be greeted with login screen like this :

Since this is our first time, we don't have any account registered yet. With that said, we can use the default user to access web interface:
```
username: shiori
password: gopher
```
Once login succeed you will be able to use the web interface. To add the new account, open the settings page and add accounts as needed:

When searching for bookmarks, you may use `tag:tagname` to include tags and `-tag:tagname` to exclude tags in the search bar. You can also use tags dialog to do this :
- `Click` on the tag name to include it;
- `Alt + Click` on the tag name to exclude it.
## Community contributions
### Improved import from Pocket
Shiori offers a [Command Line Interface](https://github.com/go-shiori/shiori/blob/master/docs/Usage.md#using-command-line-interface) with the command `shiori pocket` to import Pocket entries but with this can only import them as links and not as complete entries.
To import your bookmarks from [Pocket](https://getpocket.com/) with the text and images follow these simple steps (based on [Issue 252](https://github.com/go-shiori/shiori/issues/252)):
1. Export your entries from Pocket by visiting https://getpocket.com/export
2. Download [this shell script](https://gist.github.com/fmartingr/88a258bfad47fb00a3ef9d6c38e5699e). [*You need to download this in your docker container or on the device that you are hosting shiori*]. Name it for instance `pocket2shiori.sh`.
> Tip: checkout the documentation for [opening a console in the docker container](https://github.com/go-shiori/shiori/blob/master/docs/Usage.md#running-docker-container).
3. Execute the shell script.
Here are the commands you need to run:
```sh
wget 'https://gist.githubusercontent.com/fmartingr/88a258bfad47fb00a3ef9d6c38e5699e/raw/a21afb20b56d5383b8b975410e0eb538de02b422/pocket2shiori.sh'
chmod +x pocket2shiori.sh
pocket2shiori.sh 'path_to_your/pocket_export.html'
```
> Tip: If you’re using shiori's docker container, ensure that the exported HTML from pocket is accessible inside the docker container.
You should now see `shiori` importing your Pocket entries properly with the text and images.
This is optional, but once the import is complete you can clean up by running:
```sh
rm pocket2shiori.sh 'path_to_your/pocket_export.html'
```
### Import from Wallabag
1. Export your entries from Wallabag as a json file
2. Install [jq](https://stedolan.github.io/jq/download/). You will need this installed before running the script.
3. Download the shell script
[here](https://gist.githubusercontent.com/Aerex/01499c66f6b36a5d997f97ca1b0ab5b1/raw/bf793515540278fc675c7769be74a77ca8a41e62/wallabag2shiori). Similar to the `pocket2shiori.sh` script if you are shiori is in a docker container you will next to run this script
inside the container.
4. Execute the script. Here are the commands that you can run.
```sh
curl -sSOL
https://gist.githubusercontent.com/Aerex/01499c66f6b36a5d997f97ca1b0ab5b1/raw/bf793515540278fc675c7769be74a77ca8a41e62/wallabag2shiori'
chmod +x wallabag2shiori
./wallabag2shiori 'path/to/to/wallabag_export_json_file'
```
### Add URL to Shiori from Android
1. Install [Termux](https://termux.dev/en/)
2. Open termux and run bellow command
```bash
mkdir -p ~/bin
touch ~/bin/termux-url-opener
chmod +x ~/bin/termux-url-opener
nano ~/bin/termux-url-opener
```
3. Edit bellow code and replace `Shiori_URL`, `Username`, `Password` with yours
```bash
#!/bin/bash
# shiori settings
Shiori_URL="http://127.0.0.1:8080"
Username="shiori"
Password="gopher"
token=$(curl -s -X POST -H "Content-Type: application/json" -d '{"username": "'"$Username"'" , "password": "'"$Password"'", "remember": true}' $Shiori_URL/api/v1/auth/login | grep -oP '(?<="token":")[^"]*')
curl -s -X POST -H "Content-Type: application/json" -H "Authorization: Bearer $token" -d '{ "url": "'"$1"'", "createArchive": false, "public": 1, "tags": [], "title": "", "excerpt": "" }' $Shiori_URL/api/bookmarks
exit
```
4. Paste above content in editor and `Volume-down` and `o` than Enter to save file.
5. `Volume-down` and `x` to exit editor.
6. close termux
You can share links with termux from Share menu links will automatically add to Shiori from mobile device.
================================================
FILE: docs/assets/css/style.css
================================================
[data-md-color-scheme="shiori"] {
--md-primary-fg-color: rgb(244, 67, 54);
}
================================================
FILE: docs/faq.md
================================================
# Frequently asked questions
<!-- TOC -->
- [General](#general)
- [What is this project ?](#what-is-this-project-)
- [How does it compare to other bookmarks manager ?](#how-does-it-compare-to-other-bookmarks-manager-)
- [What are the system requirements ?](#what-are-the-system-requirements-)
- [What is the status for this app ?](#what-is-the-status-for-this-app-)
- [Is this app actively maintained ?](#is-this-app-actively-maintained-)
- [How to make a contribution ?](#how-to-make-a-contribution-)
- [How to make a donation ?](#how-to-make-a-donation-)
- [Common Issues](#common-issues)
- [What is the default account to login at the first time ?](#what-is-the-default-account-to-login-at-the-first-time-)
- [Why my old accounts can't do anything after upgrading Shiori to v1.5.0 ?](#why-my-old-accounts-cant-do-anything-after-upgrading-shiori-to-v150-)
- [`Failed to get bookmarks: failed to fetch data: no such module: fts4` ?](#failed-to-get-bookmarks-failed-to-fetch-data-no-such-module-fts4-)
- [Advanced](#advanced)
- [How to run `shiori` on start up (Linux)?](#how-to-run-shiori-on-start-up-linux)
- [How to run `shiori` on start up (macOS)?](#how-to-run-shiori-on-start-up-macos)
<!-- /TOC -->
## General
### What is this project ?
Shiori is a bookmarks manager that built with Go. I've got the idea to make this after reading a comment on HN back in [April 2017](https://news.ycombinator.com/item?id=14203383) :
```
... for me the dream bookmark manager would be something really simple
with two commands like:
$ bookmark add http://...
That will:
a. Download a static copy of the webpage in a single HTML file, with a
PDF exported copy, that also take care of removing ads and
unrelated content from the stored content.
b. Run something like http://smmry.com/ to create a summary of the page
in few sentences and store it.
c. Use NLP techniques to extract the principle keywords and use them
as tags
And another command like:
$ bookmark search "..."
That will:
d. Not use regexp or complicated search pattern, but instead;
e. Search titles, tags, page content smartly and interactively, and;
f. Sort/filter results smartly by relevance, number of matches,
frequency, or anything else useful
g. Storing everything in a git repository or simple file structure
for easy synchronization, bonus point for browsers integrations.
```
I do like using bookmarks and those idea sounds useful to me. More importantly, it seems possible enough to do. Not too hard that it's impossible for me, but not too easy that it doesn't teach me anything. Looking back now, the only thing that I (kind of) managed to do is a, b, d and e. But it's enough for me, so it's fine I guess :laughing:.
### How does it compare to other bookmarks manager ?
To be honest I don't know. The only bookmarks manager that I've used is Pocket and the one that bundled in web browser. I do like Pocket though. However, since bookmarks is kind of sensitive data, I prefer it stays offline or in my own server.
### What are the system requirements ?
It runs in the lowest tier of Digital Ocean VPS, so I guess it should be able to run anywhere.
### What is the status for this app ?
It's stable enough to use and the database shouldn't be changed anymore. However, my bookmarks at most is only several hundred entries, therefore I haven't test whether it able to process or imports huge amount of bookmarks. If you would, please do try it.
### Is this app actively maintained ?
Yes, however the development pace might be really slow. @fmartingr is the current active maintainer though @RadhiFadlillah or @deanishe may step and work on stuff from time to time or in other [go-shiori projects](https://github.com/go-shiori)
### How to make a contribution ?
Just like other open source projects, you can make a contribution by submitting issues or pull requests.
### How to make a donation ?
If you like this project, you can donate to maintainers via:
- **fmartingr** [PayPal](https://www.paypal.me/fmartingr), [Ko-Fi](https://ko-fi.com/fmartingr)
- **RadhiFadlillah** [PayPal](https://www.paypal.me/RadhiFadlillah), [Ko-Fi](https://ko-fi.com/radhifadlillah)
## Common Issues
### What is the default account to login at the first time ?
A default account is created with the credentials:
- Username: `shiori`
- Password: `gopher`
### Why my old accounts can't do anything after upgrading Shiori to v1.5.0 ?
This issue happened because in Shiori v1.0.0 there are no account level, which means everyone is treated as owner. However, in Shiori v1.5.0 there are two account levels i.e. owner and visitor. The level difference is stored in [database](https://github.com/go-shiori/shiori/blob/master/internal/database/sqlite.go#L42-L48) as boolean value in column `owner` with default value false (which means by default all account is visitor, unless specified otherwise).
Because in v1.5.0 by default all account is visitor, when updating from v1.0 to v1.5 all of the old accounts by default will be marked as visitor. Fortunately, when there are no owner registered in database, we can login as owner using default account.
So, as workaround for this issue, you should :
- Login as default account.
- Go to options page.
- Remove your old accounts.
- Recreate them, but now as owner.
For more details see [#148](https://github.com/go-shiori/shiori/issues/148).
### `Failed to get bookmarks: failed to fetch data: no such module: fts4` ?
This happens to SQLite users that upgrade from 1.5.0 to 1.5.1 because of a breaking change. Please check the
[announcement](https://github.com/go-shiori/shiori/discussions/383) to understand how to migrate your database and move forward.
## Advanced
### How to run `shiori` on start up (Linux)?
There are several methods to run `shiori` on start up, however the most recommended is running it as a service.
1. Create a service unit for `systemd` at `/etc/systemd/system/shiori.service`.
* Shiori is run via `docker` :
```ini
[Unit]
Description=Shiori container
After=docker.service
[Service]
Restart=always
ExecStartPre=-/usr/bin/docker rm shiori-1
ExecStart=/usr/bin/docker run \
--rm \
--name shiori-1 \
-p 8080:8080 \
-v /srv/machines/shiori:/shiori \
ghcr.io/go-shiori/shiori
ExecStop=/usr/bin/docker stop -t 2 shiori-1
[Install]
WantedBy=multi-user.target
```
* Shiori without `docker`. Set absolute path to `shiori` binary. `--portable` sets the data directory to be alongside the executable.
```ini
[Unit]
Description=Shiori service
[Service]
ExecStart=/home/user/go/bin/shiori server --portable
Restart=always
[Install]
WantedBy=multi-user.target
```
* Shiori without `docker` and without `--portable` but secure.
```ini
[Unit]
Description=shiori service
Requires=network-online.target
After=network-online.target
[Service]
Type=simple
ExecStart=/usr/bin/shiori server
Restart=always
User=shiori
Group=shiori
Environment="SHIORI_DIR=/var/lib/shiori"
DynamicUser=true
PrivateUsers=true
ProtectHome=true
ProtectKernelLogs=true
RestrictAddressFamilies=AF_INET AF_INET6
StateDirectory=shiori
SystemCallErrorNumber=EPERM
SystemCallFilter=@system-service
SystemCallFilter=~@chown
SystemCallFilter=~@keyring
SystemCallFilter=~@memlock
SystemCallFilter=~@setuid
DeviceAllow=
CapabilityBoundingSet=
LockPersonality=true
MemoryDenyWriteExecute=true
NoNewPrivileges=true
PrivateDevices=true
PrivateTmp=true
ProtectControlGroups=true
ProtectKernelTunables=true
ProtectSystem=full
ProtectClock=true
ProtectKernelModules=true
ProtectProc=noaccess
ProtectHostname=true
ProcSubset=pid
RestrictNamespaces=true
RestrictRealtime=true
RestrictSUIDSGID=true
SystemCallArchitectures=native
SystemCallFilter=~@clock
SystemCallFilter=~@debug
SystemCallFilter=~@module
SystemCallFilter=~@mount
SystemCallFilter=~@raw-io
SystemCallFilter=~@reboot
SystemCallFilter=~@privileged
SystemCallFilter=~@resources
SystemCallFilter=~@cpu-emulation
SystemCallFilter=~@obsolete
UMask=0077
[Install]
WantedBy=multi-user.target
```
2. Set up data directory if Shiori with `docker`
This assumes, that the Shiori container has a runtime directory to store their
database, which is at `/srv/machines/shiori`. If you want to modify that,
make sure, to fix your `shiori.service` as well.
```sh
install -d /srv/machines/shiori
```
3. Enable and start the service
```sh
systemctl enable --now shiori
```
### How to run `shiori` on start up (macOS)?
Create `local.app.shiori.plist` file in `~/Library/LaunchAgents` and use the template below. Add your own secret key and paths. The filename can be anything but it's a good practice to start it with `local`:
```xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>local.app.shiori</string>
<key>EnvironmentVariables</key>
<dict>
<key>SHIORI_HTTP_SECRET_KEY</key>
<string>somerandomvalue123489</string>
</dict>
<key>ProgramArguments</key>
<array>
<string>/absolute/path/to/shiori/binary</string>
<string>server</string>
<string>--storage-directory</string>
<string>/absolute/path/to/shiori/storage/directory</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>ServiceDescription</key>
<string>Shiori Bookmarking Service</string>
</dict>
</plist>
```
You also need to update your Mac's `System Settings > General > Login Items & Extensions > Allow in the background`. Next time you log in to your Mac, the Shiori server will automatically start and the Shiori login state will persist. To remove the service, delete the `plist` file.
================================================
FILE: docs/index.md
================================================
# Documentation
Shiori is a simple bookmarks manager written in Go language. Intended as a simple clone of [Pocket](https://getpocket.com/), it can be used as both a command line and web application. Features include:
- Basic bookmarks management (add, edit, delete and search)
- Import/export bookmarks from Netscape Bookmark file
- Import from Pocket
- Simple web interface
- Offline webpage archiving
- Support for SQLite, PostgreSQL and MySQL
================================================
FILE: docs/postman/shiori.postman_collection.json
================================================
{
"info": {
"_postman_id": "aeadb2db-90b7-40f3-87d2-de76f8e8972a",
"name": "shiori",
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
},
"item": [
{
"name": "Auth",
"item": [
{
"name": "/api/login",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"name": "Content-Type",
"value": "application/json",
"type": "text"
}
],
"body": {
"mode": "raw",
"raw": "{\n\t\"username\": \"shiori\",\n\t\"password\": \"gopher\",\n\t\"remember\": true,\n\t\"owner\": true\n}"
},
"url": {
"raw": "{{host}}/api/login",
"host": [
"{{host}}"
],
"path": [
"api",
"login"
]
}
},
"response": []
},
{
"name": "/api/logout",
"request": {
"method": "POST",
"header": [
{
"key": "X-Session-Id",
"value": "{{sessionId}}",
"type": "text"
}
],
"url": {
"raw": "{{host}}/api/logout",
"host": [
"{{host}}"
],
"path": [
"api",
"logout"
]
}
},
"response": []
}
]
},
{
"name": "Tags",
"item": [
{
"name": "/api/tags",
"request": {
"method": "GET",
"header": [
{
"key": "X-Session-Id",
"type": "text",
"value": "{{sessionId}}"
}
],
"url": {
"raw": "{{host}}/api/tags",
"host": [
"{{host}}"
],
"path": [
"api",
"tags"
]
}
},
"response": []
},
{
"name": "/api/tag",
"request": {
"method": "PUT",
"header": [
{
"key": "X-Session-Id",
"type": "text",
"value": "{{sessionId}}"
},
{
"key": "Content-Type",
"name": "Content-Type",
"value": "application/json",
"type": "text"
}
],
"body": {
"mode": "raw",
"raw": "{\n\t\"id\": 1,\n \"name\": \"renamed_tag_7\"\n}"
},
"url": {
"raw": "{{host}}/api/tag",
"host": [
"{{host}}"
],
"path": [
"api",
"tag"
]
}
},
"response": []
}
]
},
{
"name": "Bookmarks",
"item": [
{
"name": "/api/bookmarks",
"request": {
"method": "GET",
"header": [
{
"key": "X-Session-Id",
"value": "{{sessionId}}",
"type": "text"
}
],
"url": {
"raw": "{{host}}/api/bookmarks",
"host": [
"{{host}}"
],
"path": [
"api",
"bookmarks"
]
}
},
"response": []
},
{
"name": "/api/bookmarks",
"request": {
"method": "POST",
"header": [
{
"key": "X-Session-Id",
"type": "text",
"value": "{{sessionId}}"
},
{
"key": "Content-Type",
"name": "Content-Type",
"value": "application/json",
"type": "text"
}
],
"body": {
"mode": "raw",
"raw": "{\n\t\"url\": \"https://hckrnews.com\",\n\t\"createArchive\": false,\n\t\"public\": 1,\n\t\"tags\": [],\n\t\"title\": \"\",\n\t\"excerpt\": \"\"\n}"
},
"url": {
"raw": "{{host}}/api/bookmarks",
"host": [
"{{host}}"
],
"path": [
"api",
"bookmarks"
]
}
},
"response": []
},
{
"name": "/api/bookmarks",
"request": {
"method": "PUT",
"header": [
{
"key": "X-Session-Id",
"type": "text",
"value": "{{sessionId}}"
},
{
"key": "Content-Type",
"name": "Content-Type",
"type": "text",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"id\": 3,\n \"url\": \"https://hckrnews.com\",\n \"title\": \"Hacker News sorted by time\",\n \"excerpt\": \"An unofficial, alternative interface to Hacker News\",\n \"author\": \"Wayne Larsen\",\n \"public\": 1,\n \"modified\": \"2019-09-22 06:05:54\",\n \"imageURL\": \"/bookmark/3/thumb\",\n \"hasContent\": false,\n \"hasArchive\": false,\n \"tags\": [],\n \"createArchive\": false\n}"
},
"url": {
"raw": "{{host}}/api/bookmarks",
"host": [
"{{host}}"
],
"path": [
"api",
"bookmarks"
]
}
},
"response": []
},
{
"name": "/api/bookmarks",
"request": {
"method": "DELETE",
"header": [
{
"key": "X-Session-Id",
"type": "text",
"value": "{{sessionId}}"
},
{
"key": "Content-Type",
"name": "Content-Type",
"type": "text",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "[1]"
},
"url": {
"raw": "{{host}}/api/bookmarks",
"host": [
"{{host}}"
],
"path": [
"api",
"bookmarks"
]
}
},
"response": []
}
]
},
{
"name": "BFF",
"item": [
{
"name": "/api/cache",
"request": {
"method": "PUT",
"header": [
{
"key": "X-Session-Id",
"type": "text",
"value": "{{sessionId}}"
},
{
"key": "Content-Type",
"name": "Content-Type",
"type": "text",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\n\t\"ids\": [1, 2],\n\t\"keepMetadata\": false,\n\t\"createArchive\": false\n}"
},
"url": {
"raw": "{{host}}/api/cache",
"host": [
"{{host}}"
],
"path": [
"api",
"cache"
]
}
},
"response": []
},
{
"name": "/api/bookmarks/tags",
"request": {
"method": "PUT",
"header": [
{
"key": "X-Session-Id",
"type": "text",
"value": "{{sessionId}}"
},
{
"key": "Content-Type",
"name": "Content-Type",
"type": "text",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"ids\": [\n 1\n ],\n \"tags\": [\n {\n \"id\": 1,\n \"name\": \"new_tag\"\n }\n ]\n}"
},
"url": {
"raw": "{{host}}/api/bookmarks/tags",
"host": [
"{{host}}"
],
"path": [
"api",
"bookmarks",
"tags"
]
},
"description": "Performs bulk insertion of new tags into the specified bookmarks"
},
"response": []
}
]
},
{
"name": "Accounts",
"item": [
{
"name": "/api/accounts",
"protocolProfileBehavior": {
"disableBodyPruning": true
},
"request": {
"method": "GET",
"header": [
{
"key": "X-Session-Id",
"value": "{{sessionId}}",
"type": "text"
},
{
"key": "Content-Type",
"value": "application/json",
"type": "text"
}
],
"body": {
"mode": "raw",
"raw": ""
},
"url": {
"raw": "{{host}}/api/accounts",
"host": [
"{{host}}"
],
"path": [
"api",
"accounts"
]
}
},
"response": []
},
{
"name": "/api/accounts",
"request": {
"method": "PUT",
"header": [
{
"key": "X-Session-Id",
"value": "{{sessionId}}",
"type": "text"
},
{
"key": "Content-Type",
"value": "application/json",
"type": "text"
}
],
"body": {
"mode": "raw",
"raw": "{\n\t\"username\": \"shiori\",\n\t\"oldPassword\": \"gopher\",\n\t\"newPassword\": \"gopher\",\n\t\"owner\": true\n}"
},
"url": {
"raw": "{{host}}/api/accounts",
"host": [
"{{host}}"
],
"path": [
"api",
"accounts"
]
}
},
"response": []
},
{
"name": "/api/accounts",
"request": {
"method": "POST",
"header": [
{
"key": "X-Session-Id",
"type": "text",
"value": "{{sessionId}}"
},
{
"key": "Content-Type",
"type": "text",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\n\t\"username\": \"shiori3\",\n\t\"password\": \"gopher\",\n\t\"owner\": false\n}"
},
"url": {
"raw": "{{host}}/api/accounts",
"host": [
"{{host}}"
],
"path": [
"api",
"accounts"
]
}
},
"response": []
},
{
"name": "/api/accounts",
"request": {
"method": "DELETE",
"header": [
{
"key": "X-Session-Id",
"type": "text",
"value": "{{sessionId}}"
},
{
"key": "Content-Type",
"type": "text",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "[\"shiori\"]"
},
"url": {
"raw": "{{host}}/api/accounts",
"host": [
"{{host}}"
],
"path": [
"api",
"accounts"
]
}
},
"response": []
}
]
}
],
"event": [
{
"listen": "prerequest",
"script": {
"id": "d17b19de-37c1-472d-b919-d56e0f05f311",
"type": "text/javascript",
"exec": [
""
]
}
},
{
"listen": "test",
"script": {
"id": "a14c27ed-a4aa-4171-b5eb-ade9dd6d9dfb",
"type": "text/javascript",
"exec": [
""
]
}
}
],
"variable": [
{
"id": "822ed4ee-d050-46c7-b30e-eb16335e4de6",
"key": "host",
"value": "localhost:8080",
"type": "string"
},
{
"id": "89ec47f1-aae0-4872-86b1-4a721967c502",
"key": "sessionId",
"value": "a4cbd539-e54b-40a8-833a-58885f8397ba",
"type": "string"
}
]
}
================================================
FILE: docs/swagger/docs.go
================================================
// Package swagger Code generated by swaggo/swag. DO NOT EDIT
package swagger
import "github.com/swaggo/swag"
const docTemplate = `{
"schemes": {{ marshal .Schemes }},
"swagger": "2.0",
"info": {
"description": "{{escape .Description}}",
"title": "{{.Title}}",
"contact": {},
"version": "{{.Version}}"
},
"host": "{{.Host}}",
"basePath": "{{.BasePath}}",
"paths": {
"/api/v1/accounts": {
"get": {
"description": "List accounts",
"produces": [
"application/json"
],
"tags": [
"accounts"
],
"summary": "List accounts",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/model.AccountDTO"
}
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"type": "string"
}
}
}
},
"post": {
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"accounts"
],
"summary": "Create an account",
"responses": {
"201": {
"description": "Created",
"schema": {
"$ref": "#/definitions/model.AccountDTO"
}
},
"400": {
"description": "Bad Request"
},
"409": {
"description": "Account already exists"
},
"500": {
"description": "Internal Server Error"
}
}
}
},
"/api/v1/accounts/{id}": {
"delete": {
"produces": [
"application/json"
],
"tags": [
"accounts"
],
"summary": "Delete an account",
"parameters": [
{
"type": "integer",
"description": "Account ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"204": {
"description": "No content"
},
"400": {
"description": "Invalid ID"
},
"404": {
"description": "Account not found"
},
"500": {
"description": "Internal Server Error"
}
}
},
"patch": {
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"accounts"
],
"summary": "Update an account",
"parameters": [
{
"type": "integer",
"description": "Account ID",
"name": "id",
"in": "path",
"required": true
},
{
"description": "Account data",
"name": "account",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/api_v1.updateAccountPayload"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/model.AccountDTO"
}
},
"400": {
"description": "Invalid ID/data"
},
"404": {
"description": "Account not found"
},
"409": {
"description": "Account already exists"
},
"500": {
"description": "Internal Server Error"
}
}
}
},
"/api/v1/auth/account": {
"patch": {
"produces": [
"application/json"
],
"tags": [
"Auth"
],
"summary": "Update account information",
"parameters": [
{
"description": "Account data",
"name": "payload",
"in": "body",
"schema": {
"$ref": "#/definitions/api_v1.updateAccountPayload"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/model.Account"
}
},
"403": {
"description": "Token not provided/invalid"
}
}
}
},
"/api/v1/auth/login": {
"post": {
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Auth"
],
"summary": "Login to an account using username and password",
"parameters": [
{
"description": "Login data",
"name": "payload",
"in": "body",
"schema": {
"$ref": "#/definitions/api_v1.loginRequestPayload"
}
}
],
"responses": {
"200": {
"description": "Login successful",
"schema": {
"$ref": "#/definitions/api_v1.loginResponseMessage"
}
},
"400": {
"description": "Invalid login data"
}
}
}
},
"/api/v1/auth/logout": {
"post": {
"produces": [
"application/json"
],
"tags": [
"Auth"
],
"summary": "Logout from the current session",
"responses": {
"200": {
"description": "Logout successful"
},
"403": {
"description": "Token not provided/invalid"
}
}
}
},
"/api/v1/auth/me": {
"get": {
"produces": [
"application/json"
],
"tags": [
"Auth"
],
"summary": "Get information for the current logged in user",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/model.Account"
}
},
"403": {
"description": "Token not provided/invalid"
}
}
}
},
"/api/v1/auth/refresh": {
"post": {
"produces": [
"application/json"
],
"tags": [
"Auth"
],
"summary": "Refresh a token for an account",
"responses": {
"200": {
"description": "Refresh successful",
"schema": {
"$ref": "#/definitions/api_v1.loginResponseMessage"
}
},
"403": {
"description": "Token not provided/invalid"
}
}
}
},
"/api/v1/bookmarks/bulk/tags": {
"put": {
"produces": [
"application/json"
],
"tags": [
"Auth"
],
"summary": "Bulk update tags for multiple bookmarks.",
"parameters": [
{
"description": "Bulk Update Bookmark Tags Payload",
"name": "payload",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/api_v1.bulkUpdateBookmarkTagsPayload"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/model.BookmarkDTO"
}
}
},
"400": {
"description": "Invalid request payload"
},
"403": {
"description": "Token not provided/invalid"
},
"404": {
"description": "No bookmarks found"
}
}
}
},
"/api/v1/bookmarks/cache": {
"put": {
"produces": [
"application/json"
],
"tags": [
"Auth"
],
"summary": "Update Cache and Ebook on server.",
"parameters": [
{
"description": "Update Cache Payload",
"name": "payload",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/api_v1.updateCachePayload"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/model.BookmarkDTO"
}
},
"403": {
"description": "Token not provided/invalid"
}
}
}
},
"/api/v1/bookmarks/id/readable": {
"get": {
"produces": [
"application/json"
],
"tags": [
"Auth"
],
"summary": "Get readable version of bookmark.",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/api_v1.readableResponseMessage"
}
},
"403": {
"description": "Token not provided/invalid"
}
}
}
},
"/api/v1/bookmarks/{id}/tags": {
"get": {
"produces": [
"application/json"
],
"tags": [
"Auth"
],
"summary": "Get tags for a bookmark.",
"parameters": [
{
"type": "integer",
"description": "Bookmark ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/model.TagDTO"
}
}
},
"403": {
"description": "Token not provided/invalid"
},
"404": {
"description": "Bookmark not found"
}
}
},
"post": {
"produces": [
"application/json"
],
"tags": [
"Auth"
],
"summary": "Add a tag to a bookmark.",
"parameters": [
{
"type": "integer",
"description": "Bookmark ID",
"name": "id",
"in": "path",
"required": true
},
{
"description": "Add Tag Payload",
"name": "payload",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/api_v1.bookmarkTagPayload"
}
}
],
"responses": {
"200": {
"description": "OK"
},
"403": {
"description": "Token not provided/invalid"
},
"404": {
"description": "Bookmark or tag not found"
}
}
},
"delete": {
"produces": [
"application/json"
],
"tags": [
"Auth"
],
"summary": "Remove a tag from a bookmark.",
"parameters": [
{
"type": "integer",
"description": "Bookmark ID",
"name": "id",
"in": "path",
"required": true
},
{
"description": "Remove Tag Payload",
"name": "payload",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/api_v1.bookmarkTagPayload"
}
}
],
"responses": {
"200": {
"description": "OK"
},
"403": {
"description": "Token not provided/invalid"
},
"404": {
"description": "Bookmark not found"
}
}
}
},
"/api/v1/system/info": {
"get": {
"description": "Get general system information like Shiori version, database, and OS",
"produces": [
"application/json"
],
"tags": [
"System"
],
"summary": "Get general system information",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/api_v1.infoResponse"
}
},
"403": {
"description": "Only owners can access this endpoint"
}
}
}
},
"/api/v1/tags": {
"get": {
"description": "List all tags",
"produces": [
"application/json"
],
"tags": [
"Tags"
],
"summary": "List tags",
"parameters": [
{
"type": "boolean",
"description": "Include bookmark count for each tag",
"name": "with_bookmark_count",
"in": "query"
},
{
"type": "integer",
"description": "Filter tags by bookmark ID",
"name": "bookmark_id",
"in": "query"
},
{
"type": "string",
"description": "Search tags by name",
"name": "search",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/model.TagDTO"
}
}
},
"403": {
"description": "Authentication required"
},
"500": {
"description": "Internal server error"
}
}
},
"post": {
"description": "Create a new tag",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Tags"
],
"summary": "Create tag",
"parameters": [
{
"description": "Tag data",
"name": "tag",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/model.TagDTO"
}
}
],
"responses": {
"201": {
"description": "Created",
"schema": {
"$ref": "#/definitions/model.TagDTO"
}
},
"400": {
"description": "Invalid request"
},
"403": {
"description": "Authentication required"
},
"500": {
"description": "Internal server error"
}
}
}
},
"/api/v1/tags/{id}": {
"get": {
"description": "Get a tag by ID",
"produces": [
"application/json"
],
"tags": [
"Tags"
],
"summary": "Get tag",
"parameters": [
{
"type": "integer",
"description": "Tag ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/model.TagDTO"
}
},
"403": {
"description": "Authentication required"
},
"404": {
"description": "Tag not found"
},
"500": {
"description": "Internal server error"
}
}
},
"put": {
"description": "Update an existing tag",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Tags"
],
"summary": "Update tag",
"parameters": [
{
"type": "integer",
"description": "Tag ID",
"name": "id",
"in": "path",
"required": true
},
{
"description": "Tag data",
"name": "tag",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/model.TagDTO"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/model.TagDTO"
}
},
"400": {
"description": "Invalid request"
},
"403": {
"description": "Authentication required"
},
"404": {
"description": "Tag not found"
},
"500": {
"description": "Internal server error"
}
}
},
"delete": {
"description": "Delete a tag",
"tags": [
"Tags"
],
"summary": "Delete tag",
"parameters": [
{
"type": "integer",
"description": "Tag ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"204": {
"description": "No Content"
},
"403": {
"description": "Authentication required"
},
"404": {
"description": "Tag not found"
},
"500": {
"description": "Internal server error"
}
}
}
}
},
"definitions": {
"api_v1.bookmarkTagPayload": {
"type": "object",
"required": [
"tag_id"
],
"properties": {
"tag_id": {
"type": "integer"
}
}
},
"api_v1.bulkUpdateBookmarkTagsPayload": {
"type": "object",
"required": [
"bookmark_ids",
"tag_ids"
],
"properties": {
"bookmark_ids": {
"type": "array",
"items": {
"type": "integer"
}
},
"tag_ids": {
"type": "array",
"items": {
"type": "integer"
}
}
}
},
"api_v1.infoResponse": {
"type": "object",
"properties": {
"database": {
"type": "string"
},
"os": {
"type": "string"
},
"version": {
"type": "object",
"properties": {
"commit": {
"type": "string"
},
"date": {
"type": "string"
},
"tag": {
"type": "string"
}
}
}
}
},
"api_v1.loginRequestPayload": {
"type": "object",
"properties": {
"password": {
"type": "string"
},
"remember_me": {
"type": "boolean"
},
"username": {
"type": "string"
}
}
},
"api_v1.loginResponseMessage": {
"type": "object",
"properties": {
"expires": {
"type": "integer"
},
"token": {
"type": "string"
}
}
},
"api_v1.readableResponseMessage": {
"type": "object",
"properties": {
"content": {
"type": "string"
},
"html": {
"type": "string"
}
}
},
"api_v1.updateAccountPayload": {
"type": "object",
"properties": {
"config": {
"$ref": "#/definitions/model.UserConfig"
},
"new_password": {
"type": "string"
},
"old_password": {
"type": "string"
},
"owner": {
"type": "boolean"
},
"username": {
"type": "string"
}
}
},
"api_v1.updateCachePayload": {
"type": "object",
"required": [
"ids"
],
"properties": {
"create_archive": {
"type": "boolean"
},
"create_ebook": {
"type": "boolean"
},
"ids": {
"type": "array",
"items": {
"type": "integer"
}
},
"keep_metadata": {
"type": "boolean"
},
"skip_exist": {
"type": "boolean"
}
}
},
"model.Account": {
"type": "object",
"properties": {
"config": {
"$ref": "#/definitions/model.UserConfig"
},
"id": {
"type": "integer"
},
"owner": {
"type": "boolean"
},
"password": {
"type": "string"
},
"username": {
"type": "string"
}
}
},
"model.AccountDTO": {
"type": "object",
"properties": {
"config": {
"$ref": "#/definitions/model.UserConfig"
},
"id": {
"type": "integer"
},
"owner": {
"type": "boolean"
},
"passowrd": {
"description": "Used only to store, not to retrieve",
"type": "string"
},
"username": {
"type": "string"
}
}
},
"model.BookmarkDTO": {
"type": "object",
"properties": {
"author": {
"type": "string"
},
"create_archive": {
"description": "TODO: migrate outside the DTO",
"type": "boolean"
},
"create_ebook": {
"description": "TODO: migrate outside the DTO",
"type": "boolean"
},
"createdAt": {
"type": "string"
},
"excerpt": {
"type": "string"
},
"hasArchive": {
"type": "boolean"
},
"hasContent": {
"type": "boolean"
},
"hasEbook": {
"type": "boolean"
},
"html": {
"type": "string"
},
"id": {
"type": "integer"
},
"imageURL": {
"type": "string"
},
"modifiedAt": {
"type": "string"
},
"public": {
"type": "integer"
},
"tags": {
"type": "array",
"items": {
"$ref": "#/definitions/model.TagDTO"
}
},
"title": {
"type": "string"
},
"url": {
"type": "string"
}
}
},
"model.TagDTO": {
"type": "object",
"properties": {
"bookmark_count": {
"description": "Number of bookmarks with this tag",
"type": "integer"
},
"deleted": {
"description": "Marks when a tag is deleted from a bookmark",
"type": "boolean"
},
"id": {
"type": "integer"
},
"name": {
"type": "string"
}
}
},
"model.UserConfig": {
"type": "object",
"properties": {
"createEbook": {
"type": "boolean"
},
"hideExcerpt": {
"type": "boolean"
},
"hideThumbnail": {
"type": "boolean"
},
"keepMetadata": {
"type": "boolean"
},
"listMode": {
"type": "boolean"
},
"makePublic": {
"type": "boolean"
},
"showId": {
"type": "boolean"
},
"theme": {
"type": "string"
},
"useArchive": {
"type": "boolean"
}
}
}
}
}`
// SwaggerInfo holds exported Swagger Info so clients can modify it
var SwaggerInfo = &swag.Spec{
Version: "",
Host: "",
BasePath: "",
Schemes: []string{},
Title: "",
Description: "",
InfoInstanceName: "swagger",
SwaggerTemplate: docTemplate,
LeftDelim: "{{",
RightDelim: "}}",
}
func init() {
swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo)
}
================================================
FILE: docs/swagger/swagger.json
================================================
{
"swagger": "2.0",
"info": {
"contact": {}
},
"paths": {
"/api/v1/accounts": {
"get": {
"description": "List accounts",
"produces": [
"application/json"
],
"tags": [
"accounts"
],
"summary": "List accounts",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/model.AccountDTO"
}
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"type": "string"
}
}
}
},
"post": {
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"accounts"
],
"summary": "Create an account",
"responses": {
"201": {
"description": "Created",
"schema": {
"$ref": "#/definitions/model.AccountDTO"
}
},
"400": {
"description": "Bad Request"
},
"409": {
"description": "Account already exists"
},
"500": {
"description": "Internal Server Error"
}
}
}
},
"/api/v1/accounts/{id}": {
"delete": {
"produces": [
"application/json"
],
"tags": [
"accounts"
],
"summary": "Delete an account",
"parameters": [
{
"type": "integer",
"description": "Account ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"204": {
"description": "No content"
},
"400": {
"description": "Invalid ID"
},
"404": {
"description": "Account not found"
},
"500": {
"description": "Internal Server Error"
}
}
},
"patch": {
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"accounts"
],
"summary": "Update an account",
"parameters": [
{
"type": "integer",
"description": "Account ID",
"name": "id",
"in": "path",
"required": true
},
{
"description": "Account data",
"name": "account",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/api_v1.updateAccountPayload"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/model.AccountDTO"
}
},
"400": {
"description": "Invalid ID/data"
},
"404": {
"description": "Account not found"
},
"409": {
"description": "Account already exists"
},
"500": {
"description": "Internal Server Error"
}
}
}
},
"/api/v1/auth/account": {
"patch": {
"produces": [
"application/json"
],
"tags": [
"Auth"
],
"summary": "Update account information",
"parameters": [
{
"description": "Account data",
"name": "payload",
"in": "body",
"schema": {
"$ref": "#/definitions/api_v1.updateAccountPayload"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/model.Account"
}
},
"403": {
"description": "Token not provided/invalid"
}
}
}
},
"/api/v1/auth/login": {
"post": {
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Auth"
],
"summary": "Login to an account using username and password",
"parameters": [
{
"description": "Login data",
"name": "payload",
"in": "body",
"schema": {
"$ref": "#/definitions/api_v1.loginRequestPayload"
}
}
],
"responses": {
"200": {
"description": "Login successful",
"schema": {
"$ref": "#/definitions/api_v1.loginResponseMessage"
}
},
"400": {
"description": "Invalid login data"
}
}
}
},
"/api/v1/auth/logout": {
"post": {
"produces": [
"application/json"
],
"tags": [
"Auth"
],
"summary": "Logout from the current session",
"responses": {
"200": {
"description": "Logout successful"
},
"403": {
"description": "Token not provided/invalid"
}
}
}
},
"/api/v1/auth/me": {
"get": {
"produces": [
"application/json"
],
"tags": [
"Auth"
],
"summary": "Get information for the current logged in user",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/model.Account"
}
},
"403": {
"description": "Token not provided/invalid"
}
}
}
},
"/api/v1/auth/refresh": {
"post": {
"produces": [
"application/json"
],
"tags": [
"Auth"
],
"summary": "Refresh a token for an account",
"responses": {
"200": {
"description": "Refresh successful",
"schema": {
"$ref": "#/definitions/api_v1.loginResponseMessage"
}
},
"403": {
"description": "Token not provided/invalid"
}
}
}
},
"/api/v1/bookmarks/bulk/tags": {
"put": {
"produces": [
"application/json"
],
"tags": [
"Auth"
],
"summary": "Bulk update tags for multiple bookmarks.",
"parameters": [
{
"description": "Bulk Update Bookmark Tags Payload",
"name": "payload",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/api_v1.bulkUpdateBookmarkTagsPayload"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/model.BookmarkDTO"
}
}
},
"400": {
"description": "Invalid request payload"
},
"403": {
"description": "Token not provided/invalid"
},
"404": {
"description": "No bookmarks found"
}
}
}
},
"/api/v1/bookmarks/cache": {
"put": {
"produces": [
"application/json"
],
"tags": [
"Auth"
],
"summary": "Update Cache and Ebook on server.",
"parameters": [
{
"description": "Update Cache Payload",
"name": "payload",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/api_v1.updateCachePayload"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/model.BookmarkDTO"
}
},
"403": {
"description": "Token not provided/invalid"
}
}
}
},
"/api/v1/bookmarks/id/readable": {
"get": {
"produces": [
"application/json"
],
"tags": [
"Auth"
],
"summary": "Get readable version of bookmark.",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/api_v1.readableResponseMessage"
}
},
"403": {
"description": "Token not provided/invalid"
}
}
}
},
"/api/v1/bookmarks/{id}/tags": {
"get": {
"produces": [
"application/json"
],
"tags": [
"Auth"
],
"summary": "Get tags for a bookmark.",
"parameters": [
{
"type": "integer",
"description": "Bookmark ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/model.TagDTO"
}
}
},
"403": {
"description": "Token not provided/invalid"
},
"404": {
"description": "Bookmark not found"
}
}
},
"post": {
"produces": [
"application/json"
],
"tags": [
"Auth"
],
"summary": "Add a tag to a bookmark.",
"parameters": [
{
"type": "integer",
"description": "Bookmark ID",
"name": "id",
"in": "path",
"required": true
},
{
"description": "Add Tag Payload",
"name": "payload",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/api_v1.bookmarkTagPayload"
}
}
],
"responses": {
"200": {
"description": "OK"
},
"403": {
"description": "Token not provided/invalid"
},
"404": {
"description": "Bookmark or tag not found"
}
}
},
"delete": {
"produces": [
"application/json"
],
"tags": [
"Auth"
],
"summary": "Remove a tag from a bookmark.",
"parameters": [
{
"type": "integer",
"description": "Bookmark ID",
"name": "id",
"in": "path",
"required": true
},
{
"description": "Remove Tag Payload",
"name": "payload",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/api_v1.bookmarkTagPayload"
}
}
],
"responses": {
"200": {
"description": "OK"
},
"403": {
"description": "Token not provided/invalid"
},
"404": {
"description": "Bookmark not found"
}
}
}
},
"/api/v1/system/info": {
"get": {
"description": "Get general system information like Shiori version, database, and OS",
"produces": [
"application/json"
],
"tags": [
"System"
],
"summary": "Get general system information",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/api_v1.infoResponse"
}
},
"403": {
"description": "Only owners can access this endpoint"
}
}
}
},
"/api/v1/tags": {
"get": {
"description": "List all tags",
"produces": [
"application/json"
],
"tags": [
"Tags"
],
"summary": "List tags",
"parameters": [
{
"type": "boolean",
"description": "Include bookmark count for each tag",
"name": "with_bookmark_count",
"in": "query"
},
{
"type": "integer",
"description": "Filter tags by bookmark ID",
"name": "bookmark_id",
"in": "query"
},
{
"type": "string",
"description": "Search tags by name",
"name": "search",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/model.TagDTO"
}
}
},
"403": {
"description": "Authentication required"
},
"500": {
"description": "Internal server error"
}
}
},
"post": {
"description": "Create a new tag",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Tags"
],
"summary": "Create tag",
"parameters": [
{
"description": "Tag data",
"name": "tag",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/model.TagDTO"
}
}
],
"responses": {
"201": {
"description": "Created",
"schema": {
"$ref": "#/definitions/model.TagDTO"
}
},
"400": {
"description": "Invalid request"
},
"403": {
"description": "Authentication required"
},
"500": {
"description": "Internal server error"
}
}
}
},
"/api/v1/tags/{id}": {
"get": {
"description": "Get a tag by ID",
"produces": [
"application/json"
],
"tags": [
"Tags"
],
"summary": "Get tag",
"parameters": [
{
"type": "integer",
"description": "Tag ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/model.TagDTO"
}
},
"403": {
"description": "Authentication required"
},
"404": {
"description": "Tag not found"
},
"500": {
"description": "Internal server error"
}
}
},
"put": {
"description": "Update an existing tag",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Tags"
],
"summary": "Update tag",
"parameters": [
{
"type": "integer",
"description": "Tag ID",
"name": "id",
"in": "path",
"required": true
},
{
"description": "Tag data",
"name": "tag",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/model.TagDTO"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/model.TagDTO"
}
},
"400": {
"description": "Invalid request"
},
"403": {
"description": "Authentication required"
},
"404": {
"description": "Tag not found"
},
"500": {
"description": "Internal server error"
}
}
},
"delete": {
"description": "Delete a tag",
"tags": [
"Tags"
],
"summary": "Delete tag",
"parameters": [
{
"type": "integer",
"description": "Tag ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"204": {
"description": "No Content"
},
"403": {
"description": "Authentication required"
},
"404": {
"description": "Tag not found"
},
"500": {
"description": "Internal server error"
}
}
}
}
},
"definitions": {
"api_v1.bookmarkTagPayload": {
"type": "object",
"required": [
"tag_id"
],
"properties": {
"tag_id": {
"type": "integer"
}
}
},
"api_v1.bulkUpdateBookmarkTagsPayload": {
"type": "object",
"required": [
"bookmark_ids",
"tag_ids"
],
"properties": {
"bookmark_ids": {
"type": "array",
"items": {
"type": "integer"
}
},
"tag_ids": {
"type": "array",
"items": {
"type": "integer"
}
}
}
},
"api_v1.infoResponse": {
"type": "object",
"properties": {
"database": {
"type": "string"
},
"os": {
"type": "string"
},
"version": {
"type": "object",
"properties": {
"commit": {
"type": "string"
},
"date": {
"type": "string"
},
"tag": {
"type": "string"
}
}
}
}
},
"api_v1.loginRequestPayload": {
"type": "object",
"properties": {
"password": {
"type": "string"
},
"remember_me": {
"type": "boolean"
},
"username": {
"type": "string"
}
}
},
"api_v1.loginResponseMessage": {
"type": "object",
"properties": {
"expires": {
"type": "integer"
},
"token": {
"type": "string"
}
}
},
"api_v1.readableResponseMessage": {
"type": "object",
"properties": {
"content": {
"type": "string"
},
"html": {
"type": "string"
}
}
},
"api_v1.updateAccountPayload": {
"type": "object",
"properties": {
"config": {
"$ref": "#/definitions/model.UserConfig"
},
"new_password": {
"type": "string"
},
"old_password": {
"type": "string"
},
"owner": {
"type": "boolean"
},
"username": {
"type": "string"
}
}
},
"api_v1.updateCachePayload": {
"type": "object",
"required": [
"ids"
],
"properties": {
"create_archive": {
"type": "boolean"
},
"create_ebook": {
"type": "boolean"
},
"ids": {
"type": "array",
"items": {
"type": "integer"
}
},
"keep_metadata": {
"type": "boolean"
},
"skip_exist": {
"type": "boolean"
}
}
},
"model.Account": {
"type": "object",
"properties": {
"config": {
"$ref": "#/definitions/model.UserConfig"
},
"id": {
"type": "integer"
},
"owner": {
"type": "boolean"
},
"password": {
"type": "string"
},
"username": {
"type": "string"
}
}
},
"model.AccountDTO": {
"type": "object",
"properties": {
"config": {
"$ref": "#/definitions/model.UserConfig"
},
"id": {
"type": "integer"
},
"owner": {
"type": "boolean"
},
"passowrd": {
"description": "Used only to store, not to retrieve",
"type": "string"
},
"username": {
"type": "string"
}
}
},
"model.BookmarkDTO": {
"type": "object",
"properties": {
"author": {
"type": "string"
},
"create_archive": {
"description": "TODO: migrate outside the DTO",
"type": "boolean"
},
"create_ebook": {
"description": "TODO: migrate outside the DTO",
"type": "boolean"
},
"createdAt": {
"type": "string"
},
"excerpt": {
"type": "string"
},
"hasArchive": {
"type": "boolean"
},
"hasContent": {
"type": "boolean"
},
"hasEbook": {
"type": "boolean"
},
"html": {
"type": "string"
},
"id": {
"type": "integer"
},
"imageURL": {
"type": "string"
},
"modifiedAt": {
"type": "string"
},
"public": {
"type": "integer"
},
"tags": {
"type": "array",
"items": {
"$ref": "#/definitions/model.TagDTO"
}
},
"title": {
"type": "string"
},
"url": {
"type": "string"
}
}
},
"model.TagDTO": {
"type": "object",
"properties": {
"bookmark_count": {
"description": "Number of bookmarks with this tag",
"type": "integer"
},
"deleted": {
"description": "Marks when a tag is deleted from a bookmark",
"type": "boolean"
},
"id": {
"type": "integer"
},
"name": {
"type": "string"
}
}
},
"model.UserConfig": {
"type": "object",
"properties": {
"createEbook": {
"type": "boolean"
},
"hideExcerpt": {
"type": "boolean"
},
"hideThumbnail": {
"type": "boolean"
},
"keepMetadata": {
"type": "boolean"
},
"listMode": {
"type": "boolean"
},
"makePublic": {
"type": "boolean"
},
"showId": {
"type": "boolean"
},
"theme": {
"type": "string"
},
"useArchive": {
"type": "boolean"
}
}
}
}
}
================================================
FILE: docs/swagger/swagger.yaml
================================================
definitions:
api_v1.bookmarkTagPayload:
properties:
tag_id:
type: integer
required:
- tag_id
type: object
api_v1.bulkUpdateBookmarkTagsPayload:
properties:
bookmark_ids:
items:
type: integer
type: array
tag_ids:
items:
type: integer
type: array
required:
- bookmark_ids
- tag_ids
type: object
api_v1.infoResponse:
properties:
database:
type: string
os:
type: string
version:
properties:
commit:
type: string
date:
type: string
tag:
type: string
type: object
type: object
api_v1.loginRequestPayload:
properties:
password:
type: string
remember_me:
type: boolean
username:
type: string
type: object
api_v1.loginResponseMessage:
properties:
expires:
type: integer
token:
type: string
type: object
api_v1.readableResponseMessage:
properties:
content:
type: string
html:
type: string
type: object
api_v1.updateAccountPayload:
properties:
config:
$ref: '#/definitions/model.UserConfig'
new_password:
type: string
old_password:
type: string
owner:
type: boolean
username:
type: string
type: object
api_v1.updateCachePayload:
properties:
create_archive:
type: boolean
create_ebook:
type: boolean
ids:
items:
type: integer
type: array
keep_metadata:
type: boolean
skip_exist:
type: boolean
required:
- ids
type: object
model.Account:
properties:
config:
$ref: '#/definitions/model.UserConfig'
id:
type: integer
owner:
type: boolean
password:
type: string
username:
type: string
type: object
model.AccountDTO:
properties:
config:
$ref: '#/definitions/model.UserConfig'
id:
type: integer
owner:
type: boolean
passowrd:
description: Used only to store, not to retrieve
type: string
username:
type: string
type: object
model.BookmarkDTO:
properties:
author:
type: string
create_archive:
description: 'TODO: migrate outside the DTO'
type: boolean
create_ebook:
description: 'TODO: migrate outside the DTO'
type: boolean
createdAt:
type: string
excerpt:
type: string
hasArchive:
type: boolean
hasContent:
type: boolean
hasEbook:
type: boolean
html:
type: string
id:
type: integer
imageURL:
type: string
modifiedAt:
type: string
public:
type: integer
tags:
items:
$ref: '#/definitions/model.TagDTO'
type: array
title:
type: string
url:
type: string
type: object
model.TagDTO:
properties:
bookmark_count:
description: Number of bookmarks with this tag
type: integer
deleted:
description: Marks when a tag is deleted from a bookmark
type: boolean
id:
type: integer
name:
type: string
type: object
model.UserConfig:
properties:
createEbook:
type: boolean
hideExcerpt:
type: boolean
hideThumbnail:
type: boolean
keepMetadata:
type: boolean
listMode:
type: boolean
makePublic:
type: boolean
showId:
type: boolean
theme:
type: string
useArchive:
type: boolean
type: object
info:
contact: {}
paths:
/api/v1/accounts:
get:
description: List accounts
produces:
- application/json
responses:
"200":
description: OK
schema:
items:
$ref: '#/definitions/model.AccountDTO'
type: array
"500":
description: Internal Server Error
schema:
type: string
summary: List accounts
tags:
- accounts
post:
consumes:
- application/json
produces:
- application/json
responses:
"201":
description: Created
schema:
$ref: '#/definitions/model.AccountDTO'
"400":
description: Bad Request
"409":
description: Account already exists
"500":
description: Internal Server Error
summary: Create an account
tags:
- accounts
/api/v1/accounts/{id}:
delete:
parameters:
- description: Account ID
in: path
name: id
required: true
type: integer
produces:
- application/json
responses:
"204":
description: No content
"400":
description: Invalid ID
"404":
description: Account not found
"500":
description: Internal Server Error
summary: Delete an account
tags:
- accounts
patch:
consumes:
- application/json
parameters:
- description: Account ID
in: path
name: id
required: true
type: integer
- description: Account data
in: body
name: account
required: true
schema:
$ref: '#/definitions/api_v1.updateAccountPayload'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/model.AccountDTO'
"400":
description: Invalid ID/data
"404":
description: Account not found
"409":
description: Account already exists
"500":
description: Internal Server Error
summary: Update an account
tags:
- accounts
/api/v1/auth/account:
patch:
parameters:
- description: Account data
in: body
name: payload
schema:
$ref: '#/definitions/api_v1.updateAccountPayload'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/model.Account'
"403":
description: Token not provided/invalid
summary: Update account information
tags:
- Auth
/api/v1/auth/login:
post:
consumes:
- application/json
parameters:
- description: Login data
in: body
name: payload
schema:
$ref: '#/definitions/api_v1.loginRequestPayload'
produces:
- application/json
responses:
"200":
description: Login successful
schema:
$ref: '#/definitions/api_v1.loginResponseMessage'
"400":
description: Invalid login data
summary: Login to an account using username and password
tags:
- Auth
/api/v1/auth/logout:
post:
produces:
- application/json
responses:
"200":
description: Logout successful
"403":
description: Token not provided/invalid
summary: Logout from the current session
tags:
- Auth
/api/v1/auth/me:
get:
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/model.Account'
"403":
description: Token not provided/invalid
summary: Get information for the current logged in user
tags:
- Auth
/api/v1/auth/refresh:
post:
produces:
- application/json
responses:
"200":
description: Refresh successful
schema:
$ref: '#/definitions/api_v1.loginResponseMessage'
"403":
description: Token not provided/invalid
summary: Refresh a token for an account
tags:
- Auth
/api/v1/bookmarks/{id}/tags:
delete:
parameters:
- description: Bookmark ID
in: path
name: id
required: true
type: integer
- description: Remove Tag Payload
in: body
name: payload
required: true
schema:
$ref: '#/definitions/api_v1.bookmarkTagPayload'
produces:
- application/json
responses:
"200":
description: OK
"403":
description: Token not provided/invalid
"404":
description: Bookmark not found
summary: Remove a tag from a bookmark.
tags:
- Auth
get:
parameters:
- description: Bookmark ID
in: path
name: id
required: true
type: integer
produces:
- application/json
responses:
"200":
description: OK
schema:
items:
$ref: '#/definitions/model.TagDTO'
type: array
"403":
description: Token not provided/invalid
"404":
description: Bookmark not found
summary: Get tags for a bookmark.
tags:
- Auth
post:
parameters:
- description: Bookmark ID
in: path
name: id
required: true
type: integer
- description: Add Tag Payload
in: body
name: payload
required: true
schema:
$ref: '#/definitions/api_v1.bookmarkTagPayload'
produces:
- application/json
responses:
"200":
description: OK
"403":
description: Token not provided/invalid
"404":
description: Bookmark or tag not found
summary: Add a tag to a bookmark.
tags:
- Auth
/api/v1/bookmarks/bulk/tags:
put:
parameters:
- description: Bulk Update Bookmark Tags Payload
in: body
name: payload
required: true
schema:
$ref: '#/definitions/api_v1.bulkUpdateBookmarkTagsPayload'
produces:
- application/json
responses:
"200":
description: OK
schema:
items:
$ref: '#/definitions/model.BookmarkDTO'
type: array
"400":
description: Invalid request payload
"403":
description: Token not provided/invalid
"404":
description: No bookmarks found
summary: Bulk update tags for multiple bookmarks.
tags:
- Auth
/api/v1/bookmarks/cache:
put:
parameters:
- description: Update Cache Payload
in: body
name: payload
required: true
schema:
$ref: '#/definitions/api_v1.updateCachePayload'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/model.BookmarkDTO'
"403":
description: Token not provided/invalid
summary: Update Cache and Ebook on server.
tags:
- Auth
/api/v1/bookmarks/id/readable:
get:
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/api_v1.readableResponseMessage'
"403":
description: Token not provided/invalid
summary: Get readable version of bookmark.
tags:
- Auth
/api/v1/system/info:
get:
description: Get general system information like Shiori version, database, and
OS
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/api_v1.infoResponse'
"403":
description: Only owners can access this endpoint
summary: Get general system information
tags:
- System
/api/v1/tags:
get:
description: List all tags
parameters:
- description: Include bookmark count for each tag
in: query
name: with_bookmark_count
type: boolean
- description: Filter tags by bookmark ID
in: query
name: bookmark_id
type: integer
- description: Search tags by name
in: query
name: search
type: string
produces:
- application/json
responses:
"200":
description: OK
schema:
items:
$ref: '#/definitions/model.TagDTO'
type: array
"403":
description: Authentication required
"500":
description: Internal server error
summary: List tags
tags:
- Tags
post:
consumes:
- application/json
description: Create a new tag
parameters:
- description: Tag data
in: body
name: tag
required: true
schema:
$ref: '#/definitions/model.TagDTO'
produces:
- application/json
responses:
"201":
description: Created
schema:
$ref: '#/definitions/model.TagDTO'
"400":
description: Invalid request
"403":
description: Authentication required
"500":
description: Internal server error
summary: Create tag
tags:
- Tags
/api/v1/tags/{id}:
delete:
description: Delete a tag
parameters:
- description: Tag ID
in: path
name: id
required: true
type: integer
responses:
"204":
description: No Content
"403":
description: Authentication required
"404":
description: Tag not found
"500":
description: Internal server error
summary: Delete tag
tags:
- Tags
get:
description: Get a tag by ID
parameters:
- description: Tag ID
in: path
name: id
required: true
type: integer
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/model.TagDTO'
"403":
description: Authentication required
"404":
description: Tag not found
"500":
description: Internal server error
summary: Get tag
tags:
- Tags
put:
consumes:
- application/json
description: Update an existing tag
parameters:
- description: Tag ID
in: path
name: id
required: true
type: integer
- description: Tag data
in: body
name: tag
required: true
schema:
$ref: '#/definitions/model.TagDTO'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/model.TagDTO'
"400":
description: Invalid request
"403":
description: Authentication required
"404":
description: Tag not found
"500":
description: Internal server error
summary: Update tag
tags:
- Tags
swagger: "2.0"
================================================
FILE: e2e/e2eutil/containers.go
================================================
package e2eutil
import (
"context"
"io"
"os"
"strings"
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/testcontainers/testcontainers-go"
"github.com/testcontainers/testcontainers-go/wait"
)
const (
shioriPort = "8080/tcp"
shioriExpectedStartupMessage = "started http server"
shioriExpectedStartupSeconds = 5
)
var testContainersProviderType testcontainers.ProviderType = testcontainers.ProviderDocker
func init() {
// If TESTCONTAINERS_PROVIDER is set to podman, use podman
// NOTE: This is EXPERIMENTAL since there are some issues running the e2e tests using podman,
// testcontainers implies that it supports podman but I couldn't make it run in my tests.
// YMMV.
// More info: https://golang.testcontainers.org/system_requirements/using_podman/
if os.Getenv("TESTCONTAINERS_PROVIDER") == "podman" {
testContainersProviderType = testcontainers.ProviderPodman
}
}
func newBuildArg(value string) *string {
return &value
}
type ShioriContainer struct {
t *testing.T
Container testcontainers.Container
}
func (sc *ShioriContainer) GetPort() string {
mappedPort, err := sc.Container.MappedPort(context.Background(), shioriPort)
require.NoError(sc.t, err)
return mappedPort.Port()
}
// NewShioriContainer creates a new ShioriContainer which is a wrapper around a testcontainers.Container
// with some helpers for using while running Shiori E2E tests.
func NewShioriContainer(t *testing.T, tag string) ShioriContainer {
containerDefinition := testcontainers.GenericContainerRequest{
ProviderType: testContainersProviderType,
ContainerRequest: testcontainers.ContainerRequest{
Cmd: []string{"server", "--log-level", "debug"},
ExposedPorts: []string{shioriPort},
WaitingFor: wait.ForLog(shioriExpectedStartupMessage).WithStartupTimeout(shioriExpectedStartupSeconds * time.Second),
},
Started: true,
}
if tag != "" {
containerDefinition.Image = "ghcr.io/go-shiori/shiori:" + tag
} else {
containerDefinition.FromDockerfile = testcontainers.FromDockerfile{
PrintBuildLog: false,
Context: "../..",
Dockerfile: "Dockerfile.e2e",
KeepImage: true,
BuildArgs: map[string]*string{
"ALPINE_VERSION": newBuildArg(os.Getenv("CONTAINER_ALPINE_VERSION")),
"GOLANG_VERSION": newBuildArg(os.Getenv("GOLANG_VERSION")),
},
}
}
container, err := testcontainers.GenericContainer(context.Background(), containerDefinition)
require.NoError(t, err)
t.Cleanup(func() {
// Print container logs on test failure for debugging
if t.Failed() {
printContainerLogs(t, container, "Container logs on test failure:")
}
// Terminate container with error handling
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := container.Terminate(ctx); err != nil {
// Log the error but don't fail the test cleanup
t.Logf("Warning: Failed to terminate container: %v", err)
}
})
return ShioriContainer{
t: t,
Container: container,
}
}
// printContainerLogs prints the container logs for debugging purposes
func printContainerLogs(t *testing.T, container testcontainers.Container, prefix string) {
if container == nil {
t.Logf("%s Container is nil, cannot retrieve logs", prefix)
return
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
logs, err := container.Logs(ctx)
if err != nil {
t.Logf("%s Failed to get container logs: %v", prefix, err)
return
}
defer logs.Close()
logBytes, err := io.ReadAll(logs)
if err != nil {
t.Logf("%s Failed to read container logs: %v", prefix, err)
return
}
if len(logBytes) == 0 {
t.Logf("%s No container logs available", prefix)
return
}
// Split logs into lines and add prefix
logLines := strings.Split(strings.TrimSpace(string(logBytes)), "\n")
t.Logf("%s", prefix)
for i, line := range logLines {
if line != "" {
t.Logf(" [%d] %s", i+1, line)
}
}
}
================================================
FILE: e2e/playwright/accounts_test.go
================================================
package playwright
import (
"fmt"
"testing"
"time"
"github.com/go-shiori/shiori/e2e/e2eutil"
"github.com/playwright-community/playwright-go"
"github.com/stretchr/testify/require"
)
func TestE2EAccounts(t *testing.T) {
// Start a new Shiori container
container := e2eutil.NewShioriContainer(t, "")
baseURL := fmt.Sprintf("http://localhost:%s", container.GetPort())
mainTestHelper, err := NewTestHelper(t, "main")
require.NoError(t, err)
defer mainTestHelper.Close()
t.Run("001 login as admin", func(t *testing.T) {
// Navigate to the login page
_, err = mainTestHelper.page.Goto(baseURL)
mainTestHelper.Require().NoError(t, err, "Navigate to base URL")
// Get locators for form elements
usernameLocator := mainTestHelper.page.Locator("#username")
passwordLocator := mainTestHelper.page.Locator("#password")
buttonLocator := mainTestHelper.page.Locator(".button")
// Wait for and fill the login form
mainTestHelper.Require().NoError(t, usernameLocator.WaitFor(), "Wait for username field")
mainTestHelper.Require().NoError(t, usernameLocator.Fill("shiori"), "Fill username field")
mainTestHelper.Require().NoError(t, passwordLocator.Fill("gopher"), "Fill password field")
// Click login and wait for success
mainTestHelper.Require().NoError(t, buttonLocator.Click(), "Click login button")
mainTestHelper.Require().NoError(t, mainTestHelper.page.Locator("#bookmarks-grid").WaitFor(playwright.LocatorWaitForOptions{
State: playwright.WaitForSelectorStateVisible,
Timeout: playwright.Float(1000),
}), "Wait for bookmarks section to show up")
})
t.Run("002 create new admin account", func(t *testing.T) {
// Navigate to settings page
mainTestHelper.Require().NoError(t,
mainTestHelper.page.Locator(`[title="Settings"]`).Click(),
"Click on settings button")
mainTestHelper.Require().NoError(t, mainTestHelper.page.Locator(".setting-container").WaitFor(playwright.LocatorWaitForOptions{
State: playwright.WaitForSelectorStateVisible,
Timeout: playwright.Float(1000),
}), "Wait for settings page to show up")
// Click on "Add new account" <a> element
mainTestHelper.page.Locator(`[title="Add new account"]`).Click()
mainTestHelper.page.Locator(".custom-dialog").WaitFor(playwright.LocatorWaitForOptions{
State: playwright.WaitForSelectorStateVisible,
})
// Fill modal
mainTestHelper.page.Locator(`[name="username"]`).Fill("admin2")
mainTestHelper.page.Locator(`[name="password"]`).Fill("admin2")
mainTestHelper.page.Locator(`[name="repeat_password"]`).Fill("admin2")
mainTestHelper.page.Locator(`[name="admin"]`).Check()
// Click on "Ok" button
mainTestHelper.page.Locator(`.custom-dialog-button.main`).Click()
// Wait for modal to disappear
mainTestHelper.Require().NoError(t,
mainTestHelper.page.Locator(".custom-dialog").WaitFor(playwright.LocatorWaitForOptions{
State: playwright.WaitForSelectorStateHidden,
Timeout: playwright.Float(1000),
}),
"Wait for modal to disappear")
// Refresh account list
mainTestHelper.Require().NoError(t,
mainTestHelper.page.Locator(`a[title="Refresh accounts"]`).Click(),
"Click on refresh accounts button")
mainTestHelper.Require().NoError(t,
mainTestHelper.page.Locator(".loading-overlay").WaitFor(playwright.LocatorWaitForOptions{
State: playwright.WaitForSelectorStateHidden,
Timeout: playwright.Float(1000),
}),
"Wait for loading overlay to disappear")
// Check if new account is created
accountsCount, err := mainTestHelper.page.Locator(".accounts-list li").Count()
mainTestHelper.Require().NoError(t, err, "Count accounts in list")
mainTestHelper.Require().Equal(t, 2, accountsCount, "Verify 2 accounts present after creating new admin account")
})
t.Run("003 create new user account", func(t *testing.T) {
// Click on "Add new account" <a> element
mainTestHelper.page.Locator(`[title="Add new account"]`).Click()
mainTestHelper.page.Locator(".custom-dialog").WaitFor(playwright.LocatorWaitForOptions{
State: playwright.WaitForSelectorStateVisible,
Timeout: playwright.Float(1000),
})
// Fill modal
mainTestHelper.page.Locator(`[name="username"]`).Fill("user1")
mainTestHelper.page.Locator(`[name="password"]`).Fill("user1")
mainTestHelper.page.Locator(`[name="repeat_password"]`).Fill("user1")
// Click on "Ok" button
mainTestHelper.page.Locator(`.custom-dialog-button.main`).Click()
// Wait for modal to disappear
mainTestHelper.page.Locator(".custom-dialog").WaitFor(playwright.LocatorWaitForOptions{
State: playwright.WaitForSelectorStateHidden,
Timeout: playwright.Float(1000),
})
// Refresh account list
mainTestHelper.page.Locator(`a[title="Refresh accounts"]`).Click()
mainTestHelper.page.Locator(".loading-overlay").WaitFor(playwright.LocatorWaitForOptions{
State: playwright.WaitForSelectorStateHidden,
Timeout: playwright.Float(1000),
})
// Check if new account is created
accountsCount, err := mainTestHelper.page.Locator(".accounts-list li").Count()
mainTestHelper.Require().NoError(t, err, "Failed to count accounts in list")
mainTestHelper.Require().Equal(t, 3, accountsCount, "Expected 3 accounts after creating user account")
})
t.Run("004 check admin account created successfully", func(t *testing.T) {
th, err := NewTestHelper(t, t.Name())
require.NoError(t, err, "Create test helper")
defer th.Close()
// Navigate to the login page
_, err = th.page.Goto(baseURL)
th.Require().NoError(t, err, "Navigate to base URL")
// Get locators for form elements
usernameLocator := th.page.Locator("#username")
passwordLocator := th.page.Locator("#password")
buttonLocator := th.page.Locator(".button")
// Wait for and fill the login form
th.Require().NoError(t, usernameLocator.WaitFor(), "Wait for username field")
th.Require().NoError(t, usernameLocator.Fill("admin2"), "Fill username field")
th.Require().NoError(t, passwordLocator.Fill("admin2"), "Fill password field")
// Click login and wait for success
th.Require().NoError(t, buttonLocator.Click(), "Click login button")
th.Require().NoError(t, th.page.Locator("#bookmarks-grid").WaitFor(playwright.LocatorWaitForOptions{
State: playwright.WaitForSelectorStateVisible,
Timeout: playwright.Float(1000),
}), "Wait for bookmarks section to show up")
// Navigate to settings
th.Require().NoError(t,
th.page.Locator(`[title="Settings"]`).Click(),
"Click on settings button")
th.Require().NoError(t,
th.page.Locator(".setting-container").WaitFor(playwright.LocatorWaitForOptions{
State: playwright.WaitForSelectorStateVisible,
Timeout: playwright.Float(1000),
}),
"Wait for settings page to show up")
// Check if can see system info (admin only)
visible, err := th.page.Locator(`#setting-system-info`).IsVisible()
th.Require().NoError(t, err, "Check visibility of system info section")
th.Require().True(t, visible, "Verify system info section visibility for admin user")
})
t.Run("005 check user account created successfully", func(t *testing.T) {
th, err := NewTestHelper(t, t.Name())
require.NoError(t, err, "Create test helper")
defer th.Close()
// Navigate to the login page
_, err = th.page.Goto(baseURL)
th.Require().NoError(t, err, "Navigate to base URL")
// Get locators for form elements
usernameLocator := th.page.Locator("#username")
passwordLocator := th.page.Locator("#password")
buttonLocator := th.page.Locator(".button")
// Wait for and fill the login form
th.Require().NoError(t, usernameLocator.WaitFor(), "Wait for username field")
th.Require().NoError(t, usernameLocator.Fill("user1"), "Fill username field")
th.Require().NoError(t, passwordLocator.Fill("user1"), "Fill password field")
// Click login and wait for success
th.Require().NoError(t, buttonLocator.Click(), "Click login button")
th.Require().NoError(t, th.page.Locator("#bookmarks-grid").WaitFor(playwright.LocatorWaitForOptions{
State: playwright.WaitForSelectorStateVisible,
Timeout: playwright.Float(1000),
}), "Wait for bookmarks section to show up")
// Navigate to settings
th.Require().NoError(t,
th.page.Locator(`[title="Settings"]`).Click(),
"Click on settings button")
th.Require().NoError(t, th.page.Locator(".setting-container").WaitFor(playwright.LocatorWaitForOptions{
State: playwright.WaitForSelectorStateVisible,
Timeout: playwright.Float(1000),
}), "Wait for settings page to show up")
// Check if can see system info (admin only)
visible, err := th.page.Locator(`#setting-system-info`).IsVisible()
th.Require().NoError(t, err, "Check visibility of system info section")
th.Require().False(t, visible, "Verify system info section not visible for regular user")
// My account settings is visible
visible, err = th.page.Locator(`#setting-my-account`).IsVisible()
th.Require().NoError(t, err, "Check visibility of account settings")
th.Require().True(t, visible, "Verify account settings visibility for user")
// Check change password requires current password
th.Require().NoError(t,
th.page.Locator(`li[shiori-username="user1"] a[title="Change password"]`).Click(),
"Click on change password button")
th.Require().NoError(t,
th.page.Locator(".custom-dialog").WaitFor(playwright.LocatorWaitForOptions{
State: playwright.WaitForSelectorStateVisible,
Timeout: playwright.Float(1000),
}),
"Wait for change password modal to show up")
visible, err = th.page.Locator(`[name="old_password"]`).IsVisible()
th.Require().NoError(t, err, "Check visibility of old password field")
th.Require().True(t, visible, "Verify old password field visibility when changing password")
// Fill modal
th.Require().NoError(t,
th.page.Locator(`[name="old_password"]`).Fill("user1"),
"Fill old password field")
th.Require().NoError(t,
th.page.Locator(`[name="new_password"]`).Fill("new_user1"),
"Fill new password field")
th.Require().NoError(t,
th.page.Locator(`[name="repeat_password"]`).Fill("new_user1"),
"Fill repeat password field")
// Click on "Ok" button
th.Require().NoError(t,
th.page.Locator(`.custom-dialog-button.main`).Click(),
"Click on ok button")
// Wait for modal to display text: "Password has been changed."
dialogContent := th.page.Locator(".custom-dialog-content")
th.Require().NoError(t,
dialogContent.WaitFor(playwright.LocatorWaitForOptions{
State: playwright.WaitForSelectorStateVisible,
Timeout: playwright.Float(1000),
}),
"Wait for dialog content to show up")
contentText, err := dialogContent.TextContent()
th.Require().NoError(t, err, "Get dialog content text")
th.Require().Equal(t, "Password has been changed.", contentText, "Verify password change confirmation message")
})
t.Run("006 delete user account", func(t *testing.T) {
// Click on "Delete" button
mainTestHelper.page.Locator(`li[shiori-username="user1"] a[title="Delete account"]`).Click()
mainTestHelper.page.Locator(".custom-dialog").WaitFor(playwright.LocatorWaitForOptions{
State: playwright.WaitForSelectorStateVisible,
Timeout: playwright.Float(1000),
})
// Click on "Ok" button
mainTestHelper.page.Locator(`.custom-dialog-button.main`).Click()
// Wait for modal to disappear
mainTestHelper.page.Locator(".custom-dialog").WaitFor(playwright.LocatorWaitForOptions{
State: playwright.WaitForSelectorStateHidden,
Timeout: playwright.Float(1000),
})
// Refresh account list
mainTestHelper.page.Locator(`a[title="Refresh accounts"]`).Click()
mainTestHelper.page.Locator(".loading-overlay").WaitFor(playwright.LocatorWaitForOptions{
State: playwright.WaitForSelectorStateHidden,
Timeout: playwright.Float(1000),
})
// Check if account is deleted
accountsCount, err := mainTestHelper.page.Locator(".accounts-list li").Count()
mainTestHelper.Require().NoError(t, err, "Count accounts in list")
mainTestHelper.Require().Equal(t, 2, accountsCount, "Verify 2 accounts present after creating admin account")
time.Sleep(5 * time.Second)
})
t.Run("007 change password for admin account", func(t *testing.T) {
// Click on "Change password" button
mainTestHelper.Require().NoError(t,
mainTestHelper.page.Locator(`li[shiori-username="admin2"] a[title="Change password"]`).Click(),
"Click change password button")
mainTestHelper.Require().NoError(t,
mainTestHelper.page.Locator(".custom-dialog").WaitFor(playwright.LocatorWaitForOptions{
State: playwright.WaitForSelectorStateVisible,
Timeout: playwright.Float(1000),
}),
"Wait for password dialog to appear")
// Fill modal
mainTestHelper.Require().NoError(t,
mainTestHelper.page.Locator(`[name="new_password"]`).Fill("admin3"),
"Fill new password")
mainTestHelper.Require().NoError(t,
mainTestHelper.page.Locator(`[name="repeat_password"]`).Fill("admin3"),
"Fill repeat password")
// Click on "Ok" button
mainTestHelper.Require().NoError(t,
mainTestHelper.page.Locator(`.custom-dialog-button.main`).Click(),
"Click ok button")
// Wait for modal to display text: "Password has been changed."
dialogContent := mainTestHelper.page.Locator(".custom-dialog-content")
mainTestHelper.Require().NoError(t,
dialogContent.WaitFor(playwright.LocatorWaitForOptions{
State: playwright.WaitForSelectorStateVisible,
Timeout: playwright.Float(1000),
}),
"Wait for dialog content to show up")
contentText, err := dialogContent.TextContent()
mainTestHelper.Require().NoError(t, err, "Get dialog content text")
mainTestHelper.Require().Equal(t, "Password has been changed.", contentText, "Verify password change confirmation message")
// Click on "Ok" button
mainTestHelper.Require().NoError(t,
mainTestHelper.page.Locator(`.custom-dialog-button.main`).Click(),
"Click ok button")
// Wait for modal to disappear
mainTestHelper.Require().NoError(t,
mainTestHelper.page.Locator(".custom-dialog").WaitFor(playwright.LocatorWaitForOptions{
State: playwright.WaitForSelectorStateHidden,
Timeout: playwright.Float(2
gitextract_iww9x6qw/
├── .cursorrules
├── .dockerignore
├── .github/
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug_report.md
│ │ └── feature_request.md
│ ├── dependabot.yml
│ ├── stale.yml
│ └── workflows/
│ ├── _buildx.yml
│ ├── _delete-registry-tag.yml
│ ├── _e2e.yml
│ ├── _golangci-lint.yml
│ ├── _gorelease.yml
│ ├── _mkdocs-check.yml
│ ├── _mkdocs-publish.yml
│ ├── _styles-check.yml
│ ├── _swagger-check.yml
│ ├── _test.yml
│ ├── pull_request.yml
│ ├── pull_request_closed.yml
│ ├── push.yml
│ └── version_bump.yml
├── .gitignore
├── .golangci.bck.yml
├── .golangci.yml
├── .goreleaser.yaml
├── .prettierignore
├── .prettierrc
├── CODE_OF_CONDUCT.md
├── Dockerfile
├── Dockerfile.alpine
├── Dockerfile.compose
├── Dockerfile.e2e
├── LICENSE
├── Makefile
├── Procfile
├── README.md
├── app.json
├── bun.lockb
├── codecov.yml
├── docker-compose.yaml
├── docs/
│ ├── API.md
│ ├── APIv1.md
│ ├── CLI.md
│ ├── Configuration.md
│ ├── Contribute.md
│ ├── Installation.md
│ ├── Screenshots.md
│ ├── Storage.md
│ ├── Usage.md
│ ├── assets/
│ │ └── css/
│ │ └── style.css
│ ├── faq.md
│ ├── index.md
│ ├── postman/
│ │ └── shiori.postman_collection.json
│ └── swagger/
│ ├── docs.go
│ ├── swagger.json
│ └── swagger.yaml
├── e2e/
│ ├── e2eutil/
│ │ └── containers.go
│ ├── playwright/
│ │ ├── accounts_test.go
│ │ ├── auth_test.go
│ │ ├── playwright_test.go
│ │ ├── reporter.go
│ │ └── testhelper.go
│ └── server/
│ ├── auth_test.go
│ └── basic_test.go
├── go.mod
├── go.sum
├── internal/
│ ├── assets.go
│ ├── cmd/
│ │ ├── add.go
│ │ ├── check.go
│ │ ├── delete.go
│ │ ├── export.go
│ │ ├── import.go
│ │ ├── open.go
│ │ ├── pocket.go
│ │ ├── pocket_test.go
│ │ ├── print.go
│ │ ├── root.go
│ │ ├── serve.go
│ │ ├── server.go
│ │ ├── server_test.go
│ │ ├── update.go
│ │ ├── utils.go
│ │ ├── utils_test.go
│ │ └── version.go
│ ├── config/
│ │ ├── config.go
│ │ ├── config_test.go
│ │ └── storage.go
│ ├── core/
│ │ ├── core.go
│ │ ├── download.go
│ │ ├── ebook.go
│ │ ├── ebook_test.go
│ │ ├── processing.go
│ │ ├── processing_test.go
│ │ └── url.go
│ ├── database/
│ │ ├── database.go
│ │ ├── database_tags.go
│ │ ├── database_tags_test.go
│ │ ├── database_test.go
│ │ ├── migrations/
│ │ │ ├── mysql/
│ │ │ │ ├── 0000_system_create.up.sql
│ │ │ │ ├── 0000_system_insert.up.sql
│ │ │ │ ├── 0001_initial_account.up.sql
│ │ │ │ ├── 0002_initial_bookmark.up.sql
│ │ │ │ ├── 0003_initial_tag.up.sql
│ │ │ │ ├── 0004_initial_bookmark_tag.up.sql
│ │ │ │ ├── 0005_rename_to_created_at.up.sql
│ │ │ │ ├── 0006_change_created_at_settings.up.sql
│ │ │ │ ├── 0007_add_modified_at.up.sql
│ │ │ │ ├── 0008_set_modified_at_equal_created_at.up.sql
│ │ │ │ ├── 0009_index_for_created_at.up.sql
│ │ │ │ └── 0010_index_for_modified_at.up.sql
│ │ │ ├── postgres/
│ │ │ │ ├── 0000_system.up.sql
│ │ │ │ ├── 0001_initial.up.sql
│ │ │ │ └── 0002_created_time.up.sql
│ │ │ └── sqlite/
│ │ │ ├── 0000_system.up.sql
│ │ │ ├── 0001_initial.up.sql
│ │ │ ├── 0002_denormalize_content.up.sql
│ │ │ ├── 0003_uniq_id.up.sql
│ │ │ └── 0004_created_time.up.sql
│ │ ├── migrations.go
│ │ ├── mysql.go
│ │ ├── mysql_test.go
│ │ ├── pg.go
│ │ ├── pg_test.go
│ │ ├── sqlite.go
│ │ ├── sqlite_noncgo.go
│ │ ├── sqlite_openbsd.go
│ │ └── sqlite_test.go
│ ├── dependencies/
│ │ └── dependencies.go
│ ├── domains/
│ │ ├── accounts.go
│ │ ├── accounts_test.go
│ │ ├── archiver.go
│ │ ├── auth.go
│ │ ├── auth_test.go
│ │ ├── bookmark_tags_test.go
│ │ ├── bookmarks.go
│ │ ├── bookmarks_test.go
│ │ ├── storage.go
│ │ ├── storage_test.go
│ │ ├── tags.go
│ │ └── tags_test.go
│ ├── http/
│ │ ├── handlers/
│ │ │ ├── api/
│ │ │ │ └── v1/
│ │ │ │ ├── accounts.go
│ │ │ │ ├── accounts_test.go
│ │ │ │ ├── auth.go
│ │ │ │ ├── auth_test.go
│ │ │ │ ├── bookmark_tags_test.go
│ │ │ │ ├── bookmarks.go
│ │ │ │ ├── bookmarks_test.go
│ │ │ │ ├── system.go
│ │ │ │ ├── system_test.go
│ │ │ │ ├── tags.go
│ │ │ │ └── tags_test.go
│ │ │ ├── api.go
│ │ │ ├── bookmark.go
│ │ │ ├── bookmark_test.go
│ │ │ ├── frontend.go
│ │ │ ├── frontend_test.go
│ │ │ ├── legacy.go
│ │ │ ├── legacy_test.go
│ │ │ ├── swagger.go
│ │ │ ├── swagger_test.go
│ │ │ ├── system.go
│ │ │ └── system_test.go
│ │ ├── http.go
│ │ ├── http_test.go
│ │ ├── middleware/
│ │ │ ├── auth.go
│ │ │ ├── auth_sso_proxy.go
│ │ │ ├── auth_sso_proxy_test.go
│ │ │ ├── auth_test.go
│ │ │ ├── cors.go
│ │ │ ├── cors_test.go
│ │ │ ├── logging.go
│ │ │ ├── message_response.go
│ │ │ ├── message_response_test.go
│ │ │ ├── request_id.go
│ │ │ └── request_id_test.go
│ │ ├── response/
│ │ │ ├── file.go
│ │ │ ├── file_test.go
│ │ │ ├── response.go
│ │ │ ├── response_test.go
│ │ │ ├── shortcuts.go
│ │ │ └── shortcuts_test.go
│ │ ├── server.go
│ │ ├── server_test.go
│ │ ├── templates/
│ │ │ └── templates.go
│ │ └── webcontext/
│ │ ├── auth.go
│ │ ├── auth_test.go
│ │ ├── context.go
│ │ └── keys.go
│ ├── model/
│ │ ├── account.go
│ │ ├── bookmark.go
│ │ ├── bookmark_test.go
│ │ ├── const.go
│ │ ├── database.go
│ │ ├── dependencies.go
│ │ ├── domains.go
│ │ ├── errors.go
│ │ ├── http.go
│ │ ├── legacy.go
│ │ ├── main.go
│ │ ├── ptr.go
│ │ ├── slices.go
│ │ ├── slices_test.go
│ │ ├── tag.go
│ │ ├── tag_test.go
│ │ └── validation.go
│ ├── testutil/
│ │ ├── accounts.go
│ │ ├── accounts_test.go
│ │ ├── http.go
│ │ ├── response.go
│ │ └── shiori.go
│ ├── view/
│ │ ├── 404.html
│ │ ├── archive.html
│ │ ├── assets/
│ │ │ ├── css/
│ │ │ │ ├── archive.css
│ │ │ │ └── style.css
│ │ │ ├── js/
│ │ │ │ ├── component/
│ │ │ │ │ ├── bookmark.js
│ │ │ │ │ ├── dialog.js
│ │ │ │ │ ├── eventBus.js
│ │ │ │ │ ├── login.js
│ │ │ │ │ └── pagination.js
│ │ │ │ ├── page/
│ │ │ │ │ ├── base.js
│ │ │ │ │ ├── home.js
│ │ │ │ │ └── setting.js
│ │ │ │ ├── url.js
│ │ │ │ ├── utils/
│ │ │ │ │ └── api.js
│ │ │ │ └── vue.js
│ │ │ ├── less/
│ │ │ │ ├── archive.less
│ │ │ │ ├── bookmark-item.less
│ │ │ │ ├── common.less
│ │ │ │ ├── custom-dialog.less
│ │ │ │ ├── style.less
│ │ │ │ ├── theme.less
│ │ │ │ └── variables.less
│ │ │ └── manifest.webmanifest
│ │ ├── content.html
│ │ ├── embed.go
│ │ └── index.html
│ └── webserver/
│ ├── handler-api-ext.go
│ ├── handler-api.go
│ ├── handler.go
│ ├── server.go
│ ├── utils.go
│ ├── utils_ip.go
│ └── utils_ip_test.go
├── main.go
├── mkdocs.yml
├── package.json
├── scripts/
│ ├── buildx.sh
│ ├── e2e.sh
│ ├── styles.sh
│ ├── styles_check.sh
│ ├── swagger.sh
│ ├── swagger_check.sh
│ └── test.sh
├── testdata/
│ ├── nginx.conf
│ ├── pocket-new.csv
│ └── pocket-old.csv
└── webapp/
├── .editorconfig
├── .gitattributes
├── .gitignore
├── .prettierrc.json
├── README.md
├── dist/
│ ├── assets/
│ │ ├── ArchiveView-DZOySksr.js
│ │ ├── FoldersView-B-TWh6ac.js
│ │ ├── FoldersView-tn0RQdqM.css
│ │ ├── SettingsView-BWJgD3kk.js
│ │ ├── TagsView-CmDnarVi.js
│ │ ├── index-C8c580-n.js
│ │ └── index-DoBsnBZ2.css
│ └── index.html
├── embed.go
├── env.d.ts
├── eslint.config.ts
├── index.html
├── package.json
├── src/
│ ├── App.vue
│ ├── assets/
│ │ └── main.css
│ ├── client/
│ │ ├── .openapi-generator/
│ │ │ ├── FILES
│ │ │ └── VERSION
│ │ ├── .openapi-generator-ignore
│ │ ├── apis/
│ │ │ ├── AccountsApi.ts
│ │ │ ├── AuthApi.ts
│ │ │ ├── SystemApi.ts
│ │ │ ├── TagsApi.ts
│ │ │ └── index.ts
│ │ ├── index.ts
│ │ ├── models/
│ │ │ ├── ApiV1BookmarkTagPayload.ts
│ │ │ ├── ApiV1BulkUpdateBookmarkTagsPayload.ts
│ │ │ ├── ApiV1InfoResponse.ts
│ │ │ ├── ApiV1InfoResponseVersion.ts
│ │ │ ├── ApiV1LoginRequestPayload.ts
│ │ │ ├── ApiV1LoginResponseMessage.ts
│ │ │ ├── ApiV1ReadableResponseMessage.ts
│ │ │ ├── ApiV1UpdateAccountPayload.ts
│ │ │ ├── ApiV1UpdateCachePayload.ts
│ │ │ ├── ModelAccount.ts
│ │ │ ├── ModelAccountDTO.ts
│ │ │ ├── ModelBookmarkDTO.ts
│ │ │ ├── ModelTagDTO.ts
│ │ │ ├── ModelUserConfig.ts
│ │ │ └── index.ts
│ │ └── runtime.ts
│ ├── components/
│ │ └── layout/
│ │ ├── AppLayout.vue
│ │ ├── LanguageSelector.vue
│ │ ├── Sidebar.vue
│ │ └── TopBar.vue
│ ├── locales/
│ │ ├── de.json
│ │ ├── en.json
│ │ ├── es.json
│ │ ├── fr.json
│ │ └── ja.json
│ ├── main.ts
│ ├── router/
│ │ └── index.ts
│ ├── stores/
│ │ ├── auth.ts
│ │ └── tags.ts
│ ├── utils/
│ │ └── i18n.ts
│ └── views/
│ ├── AboutView.vue
│ ├── ArchiveView.vue
│ ├── FoldersView.vue
│ ├── HomeView.vue
│ ├── LoginView.vue
│ ├── SettingsView.vue
│ └── TagsView.vue
├── tailwind.config.js
├── tsconfig.app.json
├── tsconfig.json
├── tsconfig.node.json
├── tsconfig.vitest.json
├── vite.config.ts
└── vitest.config.ts
SYMBOL INDEX (2014 symbols across 187 files)
FILE: docs/swagger/docs.go
constant docTemplate (line 6) | docTemplate = `{
function init (line 1040) | func init() {
FILE: e2e/e2eutil/containers.go
constant shioriPort (line 17) | shioriPort = "8080/tcp"
constant shioriExpectedStartupMessage (line 18) | shioriExpectedStartupMessage = "started http server"
constant shioriExpectedStartupSeconds (line 19) | shioriExpectedStartupSeconds = 5
function init (line 24) | func init() {
function newBuildArg (line 35) | func newBuildArg(value string) *string {
type ShioriContainer (line 39) | type ShioriContainer struct
method GetPort (line 45) | func (sc *ShioriContainer) GetPort() string {
function NewShioriContainer (line 53) | func NewShioriContainer(t *testing.T, tag string) ShioriContainer {
function printContainerLogs (line 103) | func printContainerLogs(t *testing.T, container testcontainers.Container...
FILE: e2e/playwright/accounts_test.go
function TestE2EAccounts (line 13) | func TestE2EAccounts(t *testing.T) {
FILE: e2e/playwright/auth_test.go
function TestAuth (line 12) | func TestAuth(t *testing.T) {
FILE: e2e/playwright/playwright_test.go
function init (line 5) | func init() {
FILE: e2e/playwright/reporter.go
type AssertionResult (line 14) | type AssertionResult struct
type TestResult (line 21) | type TestResult struct
type TestReporter (line 28) | type TestReporter struct
method AddResult (line 40) | func (r *TestReporter) AddResult(testName string, passed bool, screens...
method GenerateHTML (line 87) | func (r *TestReporter) GenerateHTML() error {
function GetReporter (line 36) | func GetReporter() *TestReporter {
FILE: e2e/playwright/testhelper.go
type TestHelper (line 17) | type TestHelper struct
method Require (line 59) | func (th *TestHelper) Require() *PlaywrightRequire {
method HandleError (line 66) | func (th *TestHelper) HandleError(t *testing.T, screenshotPath string,...
method HandleSuccess (line 71) | func (th *TestHelper) HandleSuccess(t *testing.T, message string) {
method captureScreenshot (line 82) | func (th *TestHelper) captureScreenshot(testName string) string {
method Close (line 205) | func (th *TestHelper) Close() {
function NewTestHelper (line 26) | func NewTestHelper(t require.TestingT, name string) (*TestHelper, error) {
type PlaywrightRequire (line 76) | type PlaywrightRequire struct
method Assert (line 117) | func (pr *PlaywrightRequire) Assert(t *testing.T, assertFn func() erro...
method True (line 136) | func (pr *PlaywrightRequire) True(t *testing.T, value bool, msgAndArgs...
method False (line 148) | func (pr *PlaywrightRequire) False(t *testing.T, value bool, msgAndArg...
method Equal (line 160) | func (pr *PlaywrightRequire) Equal(t *testing.T, expected, actual inte...
method NoError (line 172) | func (pr *PlaywrightRequire) NoError(t *testing.T, err error, msgAndAr...
method Error (line 184) | func (pr *PlaywrightRequire) Error(t *testing.T, err error, msgAndArgs...
method Contains (line 195) | func (pr *PlaywrightRequire) Contains(t *testing.T, text, expected str...
FILE: e2e/server/auth_test.go
function TestAuthLogin (line 12) | func TestAuthLogin(t *testing.T) {
FILE: e2e/server/basic_test.go
function TestServerBasic (line 11) | func TestServerBasic(t *testing.T) {
FILE: internal/cmd/add.go
function addCmd (line 13) | func addCmd() *cobra.Command {
function addHandler (line 31) | func addHandler(cmd *cobra.Command, args []string) {
FILE: internal/cmd/check.go
function checkCmd (line 15) | func checkCmd() *cobra.Command {
function checkHandler (line 30) | func checkHandler(cmd *cobra.Command, args []string) {
FILE: internal/cmd/delete.go
function deleteCmd (line 13) | func deleteCmd() *cobra.Command {
function deleteHandler (line 31) | func deleteHandler(cmd *cobra.Command, args []string) {
FILE: internal/cmd/export.go
function exportCmd (line 14) | func exportCmd() *cobra.Command {
function exportHandler (line 25) | func exportHandler(cmd *cobra.Command, args []string) {
FILE: internal/cmd/import.go
function importCmd (line 18) | func importCmd() *cobra.Command {
function importHandler (line 31) | func importHandler(cmd *cobra.Command, args []string) {
FILE: internal/cmd/open.go
function openCmd (line 19) | func openCmd() *cobra.Command {
function openHandler (line 38) | func openHandler(cmd *cobra.Command, args []string) {
FILE: internal/cmd/pocket.go
function pocketCmd (line 21) | func pocketCmd() *cobra.Command {
function pocketHandler (line 32) | func pocketHandler(cmd *cobra.Command, args []string) {
function parseHtmlExport (line 69) | func parseHtmlExport(ctx context.Context, db model.DB, srcFile *os.File)...
function parseCsvExport (line 114) | func parseCsvExport(ctx context.Context, db model.DB, srcFile *os.File) ...
function verifyMetadata (line 169) | func verifyMetadata(title, url, timeAddedStr, tags string) (string, stri...
function handleDuplicates (line 207) | func handleDuplicates(ctx context.Context, db model.DB, mapURL map[strin...
FILE: internal/cmd/pocket_test.go
function Test_parseCsvExport_old_format (line 12) | func Test_parseCsvExport_old_format(t *testing.T) {
FILE: internal/cmd/print.go
function printCmd (line 12) | func printCmd() *cobra.Command {
function printHandler (line 34) | func printHandler(cmd *cobra.Command, args []string) {
FILE: internal/cmd/root.go
function ShioriCmd (line 21) | func ShioriCmd() *cobra.Command {
function initShiori (line 52) | func initShiori(ctx context.Context, cmd *cobra.Command) (*config.Config...
function openDatabase (line 140) | func openDatabase(logger *logrus.Logger, ctx context.Context, cfg *confi...
function openMySQLDatabase (line 160) | func openMySQLDatabase(ctx context.Context) (model.DB, error) {
function openPostgreSQLDatabase (line 170) | func openPostgreSQLDatabase(ctx context.Context) (model.DB, error) {
FILE: internal/cmd/serve.go
function serveCmd (line 7) | func serveCmd() *cobra.Command {
FILE: internal/cmd/server.go
function newServerCommand (line 14) | func newServerCommand() *cobra.Command {
function setIfFlagChanged (line 33) | func setIfFlagChanged(flagName string, flags *pflag.FlagSet, cfg *config...
function newServerCommandHandler (line 39) | func newServerCommandHandler() func(cmd *cobra.Command, args []string) {
FILE: internal/cmd/server_test.go
function Test_setIfFlagChanged (line 11) | func Test_setIfFlagChanged(t *testing.T) {
FILE: internal/cmd/update.go
function updateCmd (line 15) | func updateCmd() *cobra.Command {
function updateHandler (line 42) | func updateHandler(cmd *cobra.Command, args []string) {
FILE: internal/cmd/utils.go
function normalizeSpace (line 34) | func normalizeSpace(str string) string {
function isURLValid (line 39) | func isURLValid(s string) bool {
function printBookmarks (line 44) | func printBookmarks(bookmarks ...model.BookmarkDTO) {
function parseStrIndices (line 82) | func parseStrIndices(indices []string) ([]int, error) {
function openBrowser (line 116) | func openBrowser(url string) error {
function getTerminalWidth (line 131) | func getTerminalWidth() int {
function validateTitle (line 136) | func validateTitle(title, fallback string) string {
function SFCallerPrettyfier (line 169) | func SFCallerPrettyfier(frame *runtime.Frame) (string, string) {
FILE: internal/cmd/utils_test.go
function Test_normalizeSpace (line 8) | func Test_normalizeSpace(t *testing.T) {
function Test_isURLValid (line 44) | func Test_isURLValid(t *testing.T) {
function Test_parseStrIndices (line 84) | func Test_parseStrIndices(t *testing.T) {
FILE: internal/cmd/version.go
function newVersionCommand (line 8) | func newVersionCommand() *cobra.Command {
function newVersionCommandHandler (line 18) | func newVersionCommandHandler() func(cmd *cobra.Command, args []string) {
FILE: internal/config/config.go
function readDotEnv (line 18) | func readDotEnv(logger *logrus.Logger) map[string]string {
type HttpConfig (line 51) | type HttpConfig struct
method SetDefaults (line 75) | func (c *HttpConfig) SetDefaults(logger *logrus.Logger) {
method IsValid (line 87) | func (c *HttpConfig) IsValid() error {
type DatabaseConfig (line 99) | type DatabaseConfig struct
type StorageConfig (line 105) | type StorageConfig struct
type Config (line 109) | type Config struct
method SetDefaults (line 119) | func (c Config) SetDefaults(logger *logrus.Logger, portableMode bool) {
method DebugConfiguration (line 138) | func (c *Config) DebugConfiguration(logger *logrus.Logger) {
method IsValid (line 164) | func (c *Config) IsValid() error {
function ParseServerConfiguration (line 173) | func ParseServerConfiguration(ctx context.Context, logger *logrus.Logger...
FILE: internal/config/config_test.go
function TestHostnameVariable (line 12) | func TestHostnameVariable(t *testing.T) {
function TestBackwardsCompatibility (line 24) | func TestBackwardsCompatibility(t *testing.T) {
function TestReadDotEnv (line 53) | func TestReadDotEnv(t *testing.T) {
function TestConfigSetDefaults (line 95) | func TestConfigSetDefaults(t *testing.T) {
function TestConfigIsValid (line 105) | func TestConfigIsValid(t *testing.T) {
FILE: internal/config/storage.go
function getStorageDirectory (line 11) | func getStorageDirectory(portableMode bool) (string, error) {
FILE: internal/core/download.go
function DownloadBookmark (line 13) | func DownloadBookmark(url string) (io.ReadCloser, string, error) {
FILE: internal/core/ebook.go
function GenerateEbook (line 17) | func GenerateEbook(deps model.Dependencies, req ProcessRequest, dstPath ...
FILE: internal/core/ebook_test.go
function TestGenerateEbook (line 18) | func TestGenerateEbook(t *testing.T) {
FILE: internal/core/processing.go
type ProcessRequest (line 31) | type ProcessRequest struct
function ProcessBookmark (line 45) | func ProcessBookmark(deps model.Dependencies, req ProcessRequest) (book ...
function DownloadBookImage (line 196) | func DownloadBookImage(deps model.Dependencies, url, dstPath string) err...
FILE: internal/core/processing_test.go
function TestDownloadBookImage (line 18) | func TestDownloadBookImage(t *testing.T) {
function TestProcessBookmark (line 71) | func TestProcessBookmark(t *testing.T) {
FILE: internal/core/url.go
function queryEncodeWithoutEmptyValues (line 12) | func queryEncodeWithoutEmptyValues(v nurl.Values) string {
function RemoveUTMParams (line 40) | func RemoveUTMParams(url string) (string, error) {
FILE: internal/database/database.go
function Connect (line 24) | func Connect(ctx context.Context, dbURL string) (model.DB, error) {
type dbbase (line 43) | type dbbase struct
method Flavor (line 49) | func (db *dbbase) Flavor() sqlbuilder.Flavor {
method ReaderDB (line 53) | func (db *dbbase) ReaderDB() *sqlx.DB {
method WriterDB (line 57) | func (db *dbbase) WriterDB() *sqlx.DB {
method withTx (line 61) | func (db *dbbase) withTx(ctx context.Context, fn func(tx *sqlx.Tx) err...
method GetContext (line 84) | func (db *dbbase) GetContext(ctx context.Context, dest any, query stri...
method Select (line 89) | func (db *dbbase) Select(dest any, query string, args ...any) error {
method SelectContext (line 93) | func (db *dbbase) SelectContext(ctx context.Context, dest any, query s...
method ExecContext (line 97) | func (db *dbbase) ExecContext(ctx context.Context, query string, args ...
method MustBegin (line 101) | func (db *dbbase) MustBegin() *sqlx.Tx {
function NewDBBase (line 105) | func NewDBBase(reader, writer *sqlx.DB, flavor sqlbuilder.Flavor) dbbase {
FILE: internal/database/database_tags.go
method GetTags (line 17) | func (db *dbbase) GetTags(ctx context.Context, opts model.DBListTagsOpti...
method AddTagToBookmark (line 76) | func (db *dbbase) AddTagToBookmark(ctx context.Context, bookmarkID int, ...
method RemoveTagFromBookmark (line 125) | func (db *dbbase) RemoveTagFromBookmark(ctx context.Context, bookmarkID ...
method TagExists (line 153) | func (db *dbbase) TagExists(ctx context.Context, tagID int) (bool, error) {
method BookmarkExists (line 176) | func (db *dbbase) BookmarkExists(ctx context.Context, bookmarkID int) (b...
FILE: internal/database/database_tags_test.go
function testGetTagsFunction (line 13) | func testGetTagsFunction(t *testing.T, db model.DB) {
function testTagBookmarkOperations (line 309) | func testTagBookmarkOperations(t *testing.T, db model.DB) {
function testTagExists (line 441) | func testTagExists(t *testing.T, db model.DB) {
function testBookmarkExists (line 465) | func testBookmarkExists(t *testing.T, db model.DB) {
function testAddTagToBookmark (line 490) | func testAddTagToBookmark(t *testing.T, db model.DB) {
function testRemoveTagFromBookmark (line 537) | func testRemoveTagFromBookmark(t *testing.T, db model.DB) {
function testTagBookmarkEdgeCases (line 594) | func testTagBookmarkEdgeCases(t *testing.T, db model.DB) {
FILE: internal/database/database_test.go
type databaseTestCase (line 13) | type databaseTestCase
type testDatabaseFactory (line 14) | type testDatabaseFactory
function testDatabase (line 16) | func testDatabase(t *testing.T, dbFactory testDatabaseFactory) {
function testBookmarkAutoIncrement (line 76) | func testBookmarkAutoIncrement(t *testing.T, db model.DB) {
function testCreateBookmark (line 98) | func testCreateBookmark(t *testing.T, db model.DB) {
function testCreateBookmarkWithContent (line 112) | func testCreateBookmarkWithContent(t *testing.T, db model.DB) {
function testCreateBookmarkWithTag (line 137) | func testCreateBookmarkWithTag(t *testing.T, db model.DB) {
function testCreateBookmarkTwice (line 159) | func testCreateBookmarkTwice(t *testing.T, db model.DB) {
function testCreateTwoDifferentBookmarks (line 177) | func testCreateTwoDifferentBookmarks(t *testing.T, db model.DB) {
function testUpdateBookmark (line 196) | func testUpdateBookmark(t *testing.T, db model.DB) {
function testUpdateBookmarkWithContent (line 217) | func testUpdateBookmarkWithContent(t *testing.T, db model.DB) {
function testGetBookmark (line 249) | func testGetBookmark(t *testing.T, db model.DB) {
function testGetBookmarkNotExistent (line 267) | func testGetBookmarkNotExistent(t *testing.T, db model.DB) {
function testGetBookmarks (line 276) | func testGetBookmarks(t *testing.T, db model.DB) {
function testGetBookmarksWithSQLCharacters (line 298) | func testGetBookmarksWithSQLCharacters(t *testing.T, db model.DB) {
function testGetBookmarksWithTags (line 328) | func testGetBookmarksWithTags(t *testing.T, db model.DB) {
function testGetBookmarksCount (line 477) | func testGetBookmarksCount(t *testing.T, db model.DB) {
function testCreateTag (line 496) | func testCreateTag(t *testing.T, db model.DB) {
function testCreateTags (line 506) | func testCreateTags(t *testing.T, db model.DB) {
function testCreateAccount (line 518) | func testCreateAccount(t *testing.T, db model.DB) {
function testDeleteAccount (line 534) | func testDeleteAccount(t *testing.T, db model.DB) {
function testDeleteNonExistantAccount (line 553) | func testDeleteNonExistantAccount(t *testing.T, db model.DB) {
function testUpdateAccount (line 559) | func testUpdateAccount(t *testing.T, db model.DB) {
function testGetAccount (line 603) | func testGetAccount(t *testing.T, db model.DB) {
function testListAccounts (line 631) | func testListAccounts(t *testing.T, db model.DB) {
function testCreateDuplicateAccount (line 668) | func testCreateDuplicateAccount(t *testing.T, db model.DB) {
function testUpdateAccountDuplicateUser (line 686) | func testUpdateAccountDuplicateUser(t *testing.T, db model.DB) {
function testListAccountsWithPassword (line 713) | func testListAccountsWithPassword(t *testing.T, db model.DB) {
function testUpdateBookmarkUpdatesModifiedTime (line 731) | func testUpdateBookmarkUpdatesModifiedTime(t *testing.T, db model.DB) {
function testGetBoomarksWithTimeFilters (line 759) | func testGetBoomarksWithTimeFilters(t *testing.T, db model.DB) {
function testGetTags (line 821) | func testGetTags(t *testing.T, db model.DB) {
function testGetTag (line 856) | func testGetTag(t *testing.T, db model.DB) {
function testGetTagNotExistent (line 874) | func testGetTagNotExistent(t *testing.T, db model.DB) {
function testUpdateTag (line 884) | func testUpdateTag(t *testing.T, db model.DB) {
function testRenameTag (line 908) | func testRenameTag(t *testing.T, db model.DB) {
function testDeleteTag (line 929) | func testDeleteTag(t *testing.T, db model.DB) {
function testDeleteTagNotExistent (line 949) | func testDeleteTagNotExistent(t *testing.T, db model.DB) {
function testSaveBookmark (line 958) | func testSaveBookmark(t *testing.T, db model.DB) {
function testBulkUpdateBookmarkTags (line 1029) | func testBulkUpdateBookmarkTags(t *testing.T, db model.DB) {
FILE: internal/database/migrations.go
type migration (line 19) | type migration struct
type txFn (line 26) | type txFn
function runInTransaction (line 29) | func runInTransaction(db *sql.DB, fn txFn) error {
function newFuncMigration (line 48) | func newFuncMigration(fromVersion, toVersion string, migrationFunc func(...
function newFileMigration (line 57) | func newFileMigration(fromVersion, toVersion, filename string) migration {
function runMigrations (line 74) | func runMigrations(ctx context.Context, db model.DB, migrations []migrat...
FILE: internal/database/migrations/mysql/0000_system_create.up.sql
type shiori_system (line 1) | CREATE TABLE IF NOT EXISTS shiori_system(
FILE: internal/database/migrations/mysql/0001_initial_account.up.sql
type account (line 1) | CREATE TABLE IF NOT EXISTS account(
FILE: internal/database/migrations/mysql/0002_initial_bookmark.up.sql
type bookmark (line 1) | CREATE TABLE IF NOT EXISTS bookmark(
FILE: internal/database/migrations/mysql/0003_initial_tag.up.sql
type tag (line 1) | CREATE TABLE IF NOT EXISTS tag(
FILE: internal/database/migrations/mysql/0004_initial_bookmark_tag.up.sql
type bookmark_tag (line 1) | CREATE TABLE IF NOT EXISTS bookmark_tag(
FILE: internal/database/migrations/mysql/0009_index_for_created_at.up.sql
type idx_created_at (line 1) | CREATE INDEX idx_created_at ON bookmark (created_at)
FILE: internal/database/migrations/mysql/0010_index_for_modified_at.up.sql
type idx_modified_at (line 1) | CREATE INDEX idx_modified_at ON bookmark (modified_at)
FILE: internal/database/migrations/postgres/0000_system.up.sql
type shiori_system (line 1) | CREATE TABLE IF NOT EXISTS shiori_system(
FILE: internal/database/migrations/postgres/0001_initial.up.sql
type account (line 1) | CREATE TABLE IF NOT EXISTS account(
type bookmark (line 10) | CREATE TABLE IF NOT EXISTS bookmark(
type tag (line 24) | CREATE TABLE IF NOT EXISTS tag(
type bookmark_tag (line 30) | CREATE TABLE IF NOT EXISTS bookmark_tag(
type bookmark_tag_bookmark_id_FK (line 37) | CREATE INDEX IF NOT EXISTS bookmark_tag_bookmark_id_FK ON bookmark_tag (...
type bookmark_tag_tag_id_FK (line 38) | CREATE INDEX IF NOT EXISTS bookmark_tag_tag_id_FK ON bookmark_tag (tag_id)
FILE: internal/database/migrations/postgres/0002_created_time.up.sql
type idx_created_at (line 15) | CREATE INDEX idx_created_at ON bookmark(created_at)
type idx_modified_at (line 16) | CREATE INDEX idx_modified_at ON bookmark(modified_at)
FILE: internal/database/migrations/sqlite/0000_system.up.sql
type shiori_system (line 1) | CREATE TABLE IF NOT EXISTS shiori_system(
FILE: internal/database/migrations/sqlite/0001_initial.up.sql
type account (line 1) | CREATE TABLE IF NOT EXISTS account(
type bookmark (line 11) | CREATE TABLE IF NOT EXISTS bookmark(
type tag (line 24) | CREATE TABLE IF NOT EXISTS tag(
type bookmark_tag (line 31) | CREATE TABLE IF NOT EXISTS bookmark_tag(
FILE: internal/database/migrations/sqlite/0003_uniq_id.up.sql
type bookmark_temp (line 2) | CREATE TABLE IF NOT EXISTS bookmark_temp(
FILE: internal/database/migrations/sqlite/0004_created_time.up.sql
type idx_created_at (line 11) | CREATE INDEX idx_created_at ON bookmark(created_at)
type idx_modified_at (line 12) | CREATE INDEX idx_modified_at ON bookmark(modified_at)
FILE: internal/database/mysql.go
type MySQLDatabase (line 75) | type MySQLDatabase struct
method Init (line 95) | func (db *MySQLDatabase) Init(ctx context.Context) error {
method Migrate (line 100) | func (db *MySQLDatabase) Migrate(ctx context.Context) error {
method GetDatabaseSchemaVersion (line 109) | func (db *MySQLDatabase) GetDatabaseSchemaVersion(ctx context.Context)...
method SetDatabaseSchemaVersion (line 121) | func (db *MySQLDatabase) SetDatabaseSchemaVersion(ctx context.Context,...
method SaveBookmarks (line 135) | func (db *MySQLDatabase) SaveBookmarks(ctx context.Context, create boo...
method GetBookmarks (line 290) | func (db *MySQLDatabase) GetBookmarks(ctx context.Context, opts model....
method getTagsForBookmark (line 415) | func (db *MySQLDatabase) getTagsForBookmark(ctx context.Context, bookm...
method GetBookmarksCount (line 436) | func (db *MySQLDatabase) GetBookmarksCount(ctx context.Context, opts m...
method DeleteBookmarks (line 524) | func (db *MySQLDatabase) DeleteBookmarks(ctx context.Context, ids ...i...
method GetBookmark (line 571) | func (db *MySQLDatabase) GetBookmark(ctx context.Context, id int, url ...
method CreateAccount (line 626) | func (db *MySQLDatabase) CreateAccount(ctx context.Context, account mo...
method UpdateAccount (line 665) | func (db *MySQLDatabase) UpdateAccount(ctx context.Context, account mo...
method ListAccounts (line 708) | func (db *MySQLDatabase) ListAccounts(ctx context.Context, opts model....
method GetAccount (line 744) | func (db *MySQLDatabase) GetAccount(ctx context.Context, id model.DBID...
method DeleteAccount (line 761) | func (db *MySQLDatabase) DeleteAccount(ctx context.Context, id model.D...
method CreateTags (line 786) | func (db *MySQLDatabase) CreateTags(ctx context.Context, tags ...model...
method CreateTag (line 828) | func (db *MySQLDatabase) CreateTag(ctx context.Context, tag model.Tag)...
method RenameTag (line 843) | func (db *MySQLDatabase) RenameTag(ctx context.Context, id int, newNam...
method GetTag (line 866) | func (db *MySQLDatabase) GetTag(ctx context.Context, id int) (model.Ta...
method UpdateTag (line 891) | func (db *MySQLDatabase) UpdateTag(ctx context.Context, tag model.Tag)...
method DeleteTag (line 914) | func (db *MySQLDatabase) DeleteTag(ctx context.Context, id int) error {
method SaveBookmark (line 963) | func (db *MySQLDatabase) SaveBookmark(ctx context.Context, bookmark mo...
method SaveBookmarkTags (line 1010) | func (db *MySQLDatabase) SaveBookmarkTags(ctx context.Context, bookmar...
method BulkUpdateBookmarkTags (line 1044) | func (db *MySQLDatabase) BulkUpdateBookmarkTags(ctx context.Context, b...
function OpenMySQLDatabase (line 80) | func OpenMySQLDatabase(ctx context.Context, connString string) (mysqlDB ...
FILE: internal/database/mysql_test.go
function init (line 16) | func init() {
function mysqlTestDatabaseFactory (line 28) | func mysqlTestDatabaseFactory(envKey string) testDatabaseFactory {
function TestMysqlsDatabase (line 67) | func TestMysqlsDatabase(t *testing.T) {
function TestMariaDBDatabase (line 71) | func TestMariaDBDatabase(t *testing.T) {
FILE: internal/database/pg.go
type PGDatabase (line 77) | type PGDatabase struct
method Init (line 97) | func (db *PGDatabase) Init(ctx context.Context) error {
method Migrate (line 102) | func (db *PGDatabase) Migrate(ctx context.Context) error {
method GetDatabaseSchemaVersion (line 111) | func (db *PGDatabase) GetDatabaseSchemaVersion(ctx context.Context) (s...
method SetDatabaseSchemaVersion (line 123) | func (db *PGDatabase) SetDatabaseSchemaVersion(ctx context.Context, ve...
method SaveBookmarks (line 139) | func (db *PGDatabase) SaveBookmarks(ctx context.Context, create bool, ...
method GetBookmarks (line 280) | func (db *PGDatabase) GetBookmarks(ctx context.Context, opts model.DBG...
method GetBookmarksCount (line 425) | func (db *PGDatabase) GetBookmarksCount(ctx context.Context, opts mode...
method DeleteBookmarks (line 527) | func (db *PGDatabase) DeleteBookmarks(ctx context.Context, ids ...int)...
method GetBookmark (line 580) | func (db *PGDatabase) GetBookmark(ctx context.Context, id int, url str...
method CreateAccount (line 638) | func (db *PGDatabase) CreateAccount(ctx context.Context, account model...
method UpdateAccount (line 677) | func (db *PGDatabase) UpdateAccount(ctx context.Context, account model...
method ListAccounts (line 720) | func (db *PGDatabase) ListAccounts(ctx context.Context, opts model.DBL...
method GetAccount (line 756) | func (db *PGDatabase) GetAccount(ctx context.Context, id model.DBID) (...
method DeleteAccount (line 773) | func (db *PGDatabase) DeleteAccount(ctx context.Context, id model.DBID...
method CreateTags (line 798) | func (db *PGDatabase) CreateTags(ctx context.Context, tags ...model.Ta...
method CreateTag (line 855) | func (db *PGDatabase) CreateTag(ctx context.Context, tag model.Tag) (m...
method RenameTag (line 870) | func (db *PGDatabase) RenameTag(ctx context.Context, id int, newName s...
method GetTag (line 893) | func (db *PGDatabase) GetTag(ctx context.Context, id int) (model.TagDT...
method UpdateTag (line 918) | func (db *PGDatabase) UpdateTag(ctx context.Context, tag model.Tag) er...
method DeleteTag (line 941) | func (db *PGDatabase) DeleteTag(ctx context.Context, id int) error {
method SaveBookmark (line 990) | func (db *PGDatabase) SaveBookmark(ctx context.Context, bookmark model...
method BulkUpdateBookmarkTags (line 1036) | func (db *PGDatabase) BulkUpdateBookmarkTags(ctx context.Context, book...
function OpenPGDatabase (line 82) | func OpenPGDatabase(ctx context.Context, connString string) (pgDB *PGDat...
FILE: internal/database/pg_test.go
function init (line 15) | func init() {
function postgresqlTestDatabaseFactory (line 22) | func postgresqlTestDatabaseFactory(_ *testing.T, ctx context.Context) (m...
function TestPostgresDatabase (line 40) | func TestPostgresDatabase(t *testing.T) {
FILE: internal/database/sqlite.go
type SQLiteDatabase (line 71) | type SQLiteDatabase struct
method withTx (line 78) | func (db *SQLiteDatabase) withTx(ctx context.Context, fn func(tx *sqlx...
method withTxRetry (line 100) | func (db *SQLiteDatabase) withTxRetry(ctx context.Context, fn func(tx ...
method Init (line 123) | func (db *SQLiteDatabase) Init(ctx context.Context) error {
method WriterDB (line 169) | func (db *SQLiteDatabase) WriterDB() *sqlx.DB {
method ReaderDB (line 174) | func (db *SQLiteDatabase) ReaderDB() *sqlx.DB {
method Migrate (line 179) | func (db *SQLiteDatabase) Migrate(ctx context.Context) error {
method GetDatabaseSchemaVersion (line 188) | func (db *SQLiteDatabase) GetDatabaseSchemaVersion(ctx context.Context...
method SetDatabaseSchemaVersion (line 200) | func (db *SQLiteDatabase) SetDatabaseSchemaVersion(ctx context.Context...
method SaveBookmarks (line 216) | func (db *SQLiteDatabase) SaveBookmarks(ctx context.Context, create bo...
method GetBookmarks (line 388) | func (db *SQLiteDatabase) GetBookmarks(ctx context.Context, opts model...
method getTagsForBookmark (line 558) | func (db *SQLiteDatabase) getTagsForBookmark(ctx context.Context, book...
method GetBookmarksCount (line 579) | func (db *SQLiteDatabase) GetBookmarksCount(ctx context.Context, opts ...
method DeleteBookmarks (line 683) | func (db *SQLiteDatabase) DeleteBookmarks(ctx context.Context, ids ......
method GetBookmark (line 754) | func (db *SQLiteDatabase) GetBookmark(ctx context.Context, id int, url...
method CreateAccount (line 813) | func (db *SQLiteDatabase) CreateAccount(ctx context.Context, account m...
method UpdateAccount (line 852) | func (db *SQLiteDatabase) UpdateAccount(ctx context.Context, account m...
method ListAccounts (line 895) | func (db *SQLiteDatabase) ListAccounts(ctx context.Context, opts model...
method GetAccount (line 931) | func (db *SQLiteDatabase) GetAccount(ctx context.Context, id model.DBI...
method DeleteAccount (line 948) | func (db *SQLiteDatabase) DeleteAccount(ctx context.Context, id model....
method CreateTags (line 973) | func (db *SQLiteDatabase) CreateTags(ctx context.Context, tags ...mode...
method CreateTag (line 1015) | func (db *SQLiteDatabase) CreateTag(ctx context.Context, tag model.Tag...
method RenameTag (line 1030) | func (db *SQLiteDatabase) RenameTag(ctx context.Context, id int, newNa...
method GetTag (line 1053) | func (db *SQLiteDatabase) GetTag(ctx context.Context, id int) (model.T...
method UpdateTag (line 1078) | func (db *SQLiteDatabase) UpdateTag(ctx context.Context, tag model.Tag...
method DeleteTag (line 1101) | func (db *SQLiteDatabase) DeleteTag(ctx context.Context, id int) error {
method SaveBookmark (line 1150) | func (db *SQLiteDatabase) SaveBookmark(ctx context.Context, bookmark m...
method BulkUpdateBookmarkTags (line 1196) | func (db *SQLiteDatabase) BulkUpdateBookmarkTags(ctx context.Context, ...
type bookmarkContent (line 162) | type bookmarkContent struct
FILE: internal/database/sqlite_noncgo.go
function OpenSQLiteDatabase (line 17) | func OpenSQLiteDatabase(ctx context.Context, databasePath string) (sqlit...
FILE: internal/database/sqlite_openbsd.go
function OpenSQLiteDatabase (line 18) | func OpenSQLiteDatabase(ctx context.Context, databasePath string) (sqlit...
FILE: internal/database/sqlite_test.go
function sqliteTestDatabaseFactory (line 14) | func sqliteTestDatabaseFactory(t *testing.T, ctx context.Context) (model...
function TestSqliteDatabase (line 30) | func TestSqliteDatabase(t *testing.T) {
function testSqliteGetBookmarksWithDash (line 41) | func testSqliteGetBookmarksWithDash(t *testing.T) {
FILE: internal/dependencies/dependencies.go
type Dependencies (line 9) | type Dependencies struct
method Logger (line 16) | func (d *Dependencies) Logger() *logrus.Logger {
method Domains (line 20) | func (d *Dependencies) Domains() model.DomainDependencies {
method Config (line 24) | func (d *Dependencies) Config() *config.Config {
method Database (line 28) | func (d *Dependencies) Database() model.DB {
type domains (line 32) | type domains struct
method Auth (line 41) | func (d *domains) Auth() model.AuthDomain { retu...
method SetAuth (line 42) | func (d *domains) SetAuth(auth model.AuthDomain) { d.au...
method Accounts (line 43) | func (d *domains) Accounts() model.AccountsDomain { retu...
method SetAccounts (line 44) | func (d *domains) SetAccounts(accounts model.AccountsDomain) { d.ac...
method Bookmarks (line 45) | func (d *domains) Bookmarks() model.BookmarksDomain { retu...
method SetBookmarks (line 46) | func (d *domains) SetBookmarks(bookmarks model.BookmarksDomain) { d.bo...
method Archiver (line 47) | func (d *domains) Archiver() model.ArchiverDomain { retu...
method SetArchiver (line 48) | func (d *domains) SetArchiver(archiver model.ArchiverDomain) { d.ar...
method Storage (line 49) | func (d *domains) Storage() model.StorageDomain { retu...
method SetStorage (line 50) | func (d *domains) SetStorage(storage model.StorageDomain) { d.st...
method Tags (line 51) | func (d *domains) Tags() model.TagsDomain { retu...
method SetTags (line 52) | func (d *domains) SetTags(tags model.TagsDomain) { d.ta...
function NewDependencies (line 56) | func NewDependencies(log *logrus.Logger, db model.DB, cfg *config.Config...
FILE: internal/domains/accounts.go
type AccountsDomain (line 14) | type AccountsDomain struct
method ListAccounts (line 18) | func (d *AccountsDomain) ListAccounts(ctx context.Context) ([]model.Ac...
method GetAccountByUsername (line 32) | func (d *AccountsDomain) GetAccountByUsername(ctx context.Context, use...
method CreateAccount (line 50) | func (d *AccountsDomain) CreateAccount(ctx context.Context, account mo...
method DeleteAccount (line 85) | func (d *AccountsDomain) DeleteAccount(ctx context.Context, id int) er...
method UpdateAccount (line 98) | func (d *AccountsDomain) UpdateAccount(ctx context.Context, account mo...
function NewAccountsDomain (line 154) | func NewAccountsDomain(deps *dependencies.Dependencies) model.AccountsDo...
FILE: internal/domains/accounts_test.go
function TestAccountDomainsListAccounts (line 15) | func TestAccountDomainsListAccounts(t *testing.T) {
function TestAccountDomainsGetAccountByUsername (line 41) | func TestAccountDomainsGetAccountByUsername(t *testing.T) {
function TestAccountDomainCreateAccount (line 65) | func TestAccountDomainCreateAccount(t *testing.T) {
function TestAccountDomainUpdateAccount (line 105) | func TestAccountDomainUpdateAccount(t *testing.T) {
function TestAccountDomainDeleteAccount (line 156) | func TestAccountDomainDeleteAccount(t *testing.T) {
FILE: internal/domains/archiver.go
type ArchiverDomain (line 13) | type ArchiverDomain struct
method DownloadBookmarkArchive (line 17) | func (d *ArchiverDomain) DownloadBookmarkArchive(book model.BookmarkDT...
method GetBookmarkArchive (line 40) | func (d *ArchiverDomain) GetBookmarkArchive(book *model.BookmarkDTO) (...
function NewArchiverDomain (line 51) | func NewArchiverDomain(deps *dependencies.Dependencies) *ArchiverDomain {
FILE: internal/domains/auth.go
type AuthDomain (line 14) | type AuthDomain struct
method CheckToken (line 24) | func (d *AuthDomain) CheckToken(ctx context.Context, userJWT string) (...
method GetAccountFromCredentials (line 47) | func (d *AuthDomain) GetAccountFromCredentials(ctx context.Context, us...
method CreateTokenForAccount (line 69) | func (d *AuthDomain) CreateTokenForAccount(account *model.AccountDTO, ...
type JWTClaim (line 18) | type JWTClaim struct
function NewAuthDomain (line 89) | func NewAuthDomain(deps *dependencies.Dependencies) *AuthDomain {
FILE: internal/domains/auth_test.go
function TestAuthDomainCheckToken (line 16) | func TestAuthDomainCheckToken(t *testing.T) {
function TestAuthDomainCheckTokenInvalidMethod (line 65) | func TestAuthDomainCheckTokenInvalidMethod(t *testing.T) {
function TestAuthDomainGetAccountFromCredentials (line 88) | func TestAuthDomainGetAccountFromCredentials(t *testing.T) {
FILE: internal/domains/bookmark_tags_test.go
function TestBookmarkTagOperations (line 14) | func TestBookmarkTagOperations(t *testing.T) {
FILE: internal/domains/bookmarks.go
type BookmarksDomain (line 11) | type BookmarksDomain struct
method HasEbook (line 15) | func (d *BookmarksDomain) HasEbook(b *model.BookmarkDTO) bool {
method HasArchive (line 20) | func (d *BookmarksDomain) HasArchive(b *model.BookmarkDTO) bool {
method HasThumbnail (line 25) | func (d *BookmarksDomain) HasThumbnail(b *model.BookmarkDTO) bool {
method GetBookmark (line 30) | func (d *BookmarksDomain) GetBookmark(ctx context.Context, id model.DB...
method GetBookmarks (line 47) | func (d *BookmarksDomain) GetBookmarks(ctx context.Context, ids []int)...
method UpdateBookmarkCache (line 66) | func (d *BookmarksDomain) UpdateBookmarkCache(ctx context.Context, boo...
method BulkUpdateBookmarkTags (line 102) | func (d *BookmarksDomain) BulkUpdateBookmarkTags(ctx context.Context, ...
method AddTagToBookmark (line 117) | func (d *BookmarksDomain) AddTagToBookmark(ctx context.Context, bookma...
method RemoveTagFromBookmark (line 141) | func (d *BookmarksDomain) RemoveTagFromBookmark(ctx context.Context, b...
method BookmarkExists (line 165) | func (d *BookmarksDomain) BookmarkExists(ctx context.Context, id int) ...
function NewBookmarksDomain (line 169) | func NewBookmarksDomain(deps model.Dependencies) *BookmarksDomain {
FILE: internal/domains/bookmarks_test.go
function TestBookmarkDomain (line 17) | func TestBookmarkDomain(t *testing.T) {
function TestBookmarksDomain_BulkUpdateBookmarkTags (line 211) | func TestBookmarksDomain_BulkUpdateBookmarkTags(t *testing.T) {
FILE: internal/domains/storage.go
type StorageDomain (line 14) | type StorageDomain struct
method Stat (line 27) | func (d *StorageDomain) Stat(name string) (fs.FileInfo, error) {
method FS (line 32) | func (d *StorageDomain) FS() afero.Fs {
method FileExists (line 37) | func (d *StorageDomain) FileExists(name string) bool {
method DirExists (line 43) | func (d *StorageDomain) DirExists(name string) bool {
method WriteData (line 50) | func (d *StorageDomain) WriteData(dst string, data []byte) error {
method WriteFile (line 73) | func (d *StorageDomain) WriteFile(dst string, tmpFile *os.File) error {
function NewStorageDomain (line 19) | func NewStorageDomain(deps model.Dependencies, fs afero.Fs) *StorageDoma...
FILE: internal/domains/storage_test.go
function TestDirExists (line 15) | func TestDirExists(t *testing.T) {
function TestFileExists (line 32) | func TestFileExists(t *testing.T) {
function TestWriteFile (line 49) | func TestWriteFile(t *testing.T) {
function TestSaveFile (line 74) | func TestSaveFile(t *testing.T) {
FILE: internal/domains/tags.go
type tagsDomain (line 11) | type tagsDomain struct
method ListTags (line 19) | func (d *tagsDomain) ListTags(ctx context.Context, opts model.ListTags...
method CreateTag (line 28) | func (d *tagsDomain) CreateTag(ctx context.Context, tagDTO model.TagDT...
method GetTag (line 38) | func (d *tagsDomain) GetTag(ctx context.Context, id int) (model.TagDTO...
method UpdateTag (line 49) | func (d *tagsDomain) UpdateTag(ctx context.Context, tagDTO model.TagDT...
method DeleteTag (line 68) | func (d *tagsDomain) DeleteTag(ctx context.Context, id int) error {
method TagExists (line 80) | func (d *tagsDomain) TagExists(ctx context.Context, id int) (bool, err...
function NewTagsDomain (line 15) | func NewTagsDomain(deps model.Dependencies) model.TagsDomain {
FILE: internal/domains/tags_test.go
function TestTagsDomain (line 17) | func TestTagsDomain(t *testing.T) {
FILE: internal/http/handlers/api.go
type APIHandler (line 10) | type APIHandler struct
method ServeHTTP (line 15) | func (h *APIHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
method handleGet (line 26) | func (h *APIHandler) handleGet(w http.ResponseWriter, r *http.Request) {
method handlePost (line 30) | func (h *APIHandler) handlePost(w http.ResponseWriter, r *http.Request) {
function NewAPIHandler (line 34) | func NewAPIHandler(logger *logrus.Logger, deps *dependencies.Dependencie...
FILE: internal/http/handlers/api/v1/accounts.go
type createAccountPayload (line 14) | type createAccountPayload struct
method ToAccountDTO (line 20) | func (p *createAccountPayload) ToAccountDTO() model.AccountDTO {
function HandleListAccounts (line 35) | func HandleListAccounts(deps model.Dependencies, c model.WebContext) {
function HandleCreateAccount (line 59) | func HandleCreateAccount(deps model.Dependencies, c model.WebContext) {
function HandleDeleteAccount (line 99) | func HandleDeleteAccount(deps model.Dependencies, c model.WebContext) {
function HandleUpdateAccount (line 137) | func HandleUpdateAccount(deps model.Dependencies, c model.WebContext) {
FILE: internal/http/handlers/api/v1/accounts_test.go
function TestHandleListAccounts (line 15) | func TestHandleListAccounts(t *testing.T) {
function TestHandleCreateAccount (line 67) | func TestHandleCreateAccount(t *testing.T) {
function TestHandleDeleteAccount (line 156) | func TestHandleDeleteAccount(t *testing.T) {
function TestHandleUpdateAccount (line 211) | func TestHandleUpdateAccount(t *testing.T) {
FILE: internal/http/handlers/api/v1/auth.go
type loginRequestPayload (line 14) | type loginRequestPayload struct
method IsValid (line 20) | func (p *loginRequestPayload) IsValid() error {
type loginResponseMessage (line 30) | type loginResponseMessage struct
function HandleLogin (line 43) | func HandleLogin(deps model.Dependencies, c model.WebContext) {
function HandleRefreshToken (line 87) | func HandleRefreshToken(deps model.Dependencies, c model.WebContext) {
function HandleGetMe (line 113) | func HandleGetMe(deps model.Dependencies, c model.WebContext) {
type updateAccountPayload (line 120) | type updateAccountPayload struct
method IsValid (line 128) | func (p *updateAccountPayload) IsValid() error {
method ToAccountDTO (line 135) | func (p *updateAccountPayload) ToAccountDTO() model.AccountDTO {
function HandleUpdateLoggedAccount (line 162) | func HandleUpdateLoggedAccount(deps model.Dependencies, c model.WebConte...
function HandleLogout (line 211) | func HandleLogout(deps model.Dependencies, c model.WebContext) {
FILE: internal/http/handlers/api/v1/auth_test.go
function TestHandleLogin (line 14) | func TestHandleLogin(t *testing.T) {
function TestHandleRefreshToken (line 78) | func TestHandleRefreshToken(t *testing.T) {
function TestHandleGetMe (line 107) | func TestHandleGetMe(t *testing.T) {
function TestHandleUpdateLoggedAccount (line 150) | func TestHandleUpdateLoggedAccount(t *testing.T) {
function TestHandleLogout (line 223) | func TestHandleLogout(t *testing.T) {
FILE: internal/http/handlers/api/v1/bookmark_tags_test.go
type bookmarkTagPayload (line 19) | type bookmarkTagPayload struct
function TestBookmarkTagsAPI (line 23) | func TestBookmarkTagsAPI(t *testing.T) {
FILE: internal/http/handlers/api/v1/bookmarks.go
type updateCachePayload (line 16) | type updateCachePayload struct
method IsValid (line 24) | func (p *updateCachePayload) IsValid() error {
type readableResponseMessage (line 36) | type readableResponseMessage struct
function HandleBookmarkReadable (line 50) | func HandleBookmarkReadable(deps model.Dependencies, c model.WebContext) {
function HandleUpdateCache (line 84) | func HandleUpdateCache(deps model.Dependencies, c model.WebContext) {
type bulkUpdateBookmarkTagsPayload (line 165) | type bulkUpdateBookmarkTagsPayload struct
method IsValid (line 170) | func (p *bulkUpdateBookmarkTagsPayload) IsValid() error {
function HandleGetBookmarkTags (line 191) | func HandleGetBookmarkTags(deps model.Dependencies, c model.WebContext) {
type bookmarkTagPayload (line 227) | type bookmarkTagPayload struct
method IsValid (line 231) | func (p *bookmarkTagPayload) IsValid() error {
function HandleAddTagToBookmark (line 250) | func HandleAddTagToBookmark(deps model.Dependencies, c model.WebContext) {
function HandleRemoveTagFromBookmark (line 304) | func HandleRemoveTagFromBookmark(deps model.Dependencies, c model.WebCon...
function HandleBulkUpdateBookmarkTags (line 358) | func HandleBulkUpdateBookmarkTags(deps model.Dependencies, c model.WebCo...
FILE: internal/http/handlers/api/v1/bookmarks_test.go
function TestHandleBookmarkReadable (line 18) | func TestHandleBookmarkReadable(t *testing.T) {
function TestHandleUpdateCache (line 92) | func TestHandleUpdateCache(t *testing.T) {
function TestHandleUpdateBookmarkTags (line 201) | func TestHandleUpdateBookmarkTags(t *testing.T) {
FILE: internal/http/handlers/api/v1/system.go
type infoResponse (line 12) | type infoResponse struct
function HandleSystemInfo (line 30) | func HandleSystemInfo(deps model.Dependencies, c model.WebContext) {
FILE: internal/http/handlers/api/v1/system_test.go
function TestHandleSystemInfo (line 13) | func TestHandleSystemInfo(t *testing.T) {
FILE: internal/http/handlers/api/v1/tags.go
function HandleListTags (line 25) | func HandleListTags(deps model.Dependencies, c model.WebContext) {
function HandleGetTag (line 78) | func HandleGetTag(deps model.Dependencies, c model.WebContext) {
function HandleCreateTag (line 116) | func HandleCreateTag(deps model.Dependencies, c model.WebContext) {
function HandleUpdateTag (line 157) | func HandleUpdateTag(deps model.Dependencies, c model.WebContext) {
function HandleDeleteTag (line 208) | func HandleDeleteTag(deps model.Dependencies, c model.WebContext) {
FILE: internal/http/handlers/api/v1/tags_test.go
function TestHandleListTags (line 16) | func TestHandleListTags(t *testing.T) {
function TestHandleGetTag (line 243) | func TestHandleGetTag(t *testing.T) {
function TestHandleCreateTag (line 318) | func TestHandleCreateTag(t *testing.T) {
function TestHandleUpdateTag (line 379) | func TestHandleUpdateTag(t *testing.T) {
function TestHandleDeleteTag (line 487) | func TestHandleDeleteTag(t *testing.T) {
FILE: internal/http/handlers/bookmark.go
function getBookmark (line 15) | func getBookmark(deps model.Dependencies, c model.WebContext) (*model.Bo...
function HandleBookmarkContent (line 41) | func HandleBookmarkContent(deps model.Dependencies, c model.WebContext) {
function HandleBookmarkArchive (line 60) | func HandleBookmarkArchive(deps model.Dependencies, c model.WebContext) {
function HandleBookmarkArchiveFile (line 83) | func HandleBookmarkArchiveFile(deps model.Dependencies, c model.WebConte...
function HandleBookmarkThumbnail (line 129) | func HandleBookmarkThumbnail(deps model.Dependencies, c model.WebContext) {
function HandleBookmarkEbook (line 160) | func HandleBookmarkEbook(deps model.Dependencies, c model.WebContext) {
FILE: internal/http/handlers/bookmark_test.go
function TestGetBookmark (line 16) | func TestGetBookmark(t *testing.T) {
function TestBookmarkContentHandler (line 74) | func TestBookmarkContentHandler(t *testing.T) {
function TestBookmarkFileHandlers (line 104) | func TestBookmarkFileHandlers(t *testing.T) {
FILE: internal/http/handlers/frontend.go
type assetsFS (line 14) | type assetsFS struct
method Open (line 19) | func (fs assetsFS) Open(name string) (http.File, error) {
function newAssetsFS (line 28) | func newAssetsFS(fs embed.FS, serveWebUIV2 bool) http.FileSystem {
function HandleFrontend (line 36) | func HandleFrontend(deps model.Dependencies, c model.WebContext) {
function HandleAssets (line 48) | func HandleAssets(deps model.Dependencies, c model.WebContext) {
FILE: internal/http/handlers/frontend_test.go
function TestHandleFrontend (line 14) | func TestHandleFrontend(t *testing.T) {
function TestHandleAssets (line 29) | func TestHandleAssets(t *testing.T) {
FILE: internal/http/handlers/legacy.go
type LegacyHandler (line 14) | type LegacyHandler struct
method convertParams (line 33) | func (h *LegacyHandler) convertParams(r *http.Request) httprouter.Para...
method HandleLogin (line 46) | func (h *LegacyHandler) HandleLogin(account *model.AccountDTO, expTime...
method HandleLogout (line 58) | func (h *LegacyHandler) HandleLogout(deps model.Dependencies, c model....
method HandleGetTags (line 68) | func (h *LegacyHandler) HandleGetTags(deps model.Dependencies, c model...
method HandleRenameTag (line 73) | func (h *LegacyHandler) HandleRenameTag(deps model.Dependencies, c mod...
method HandleGetBookmarks (line 78) | func (h *LegacyHandler) HandleGetBookmarks(deps model.Dependencies, c ...
method HandleInsertBookmark (line 83) | func (h *LegacyHandler) HandleInsertBookmark(deps model.Dependencies, ...
method HandleDeleteBookmark (line 88) | func (h *LegacyHandler) HandleDeleteBookmark(deps model.Dependencies, ...
method HandleUpdateBookmark (line 93) | func (h *LegacyHandler) HandleUpdateBookmark(deps model.Dependencies, ...
method HandleUpdateBookmarkTags (line 98) | func (h *LegacyHandler) HandleUpdateBookmarkTags(deps model.Dependenci...
method HandleInsertViaExtension (line 103) | func (h *LegacyHandler) HandleInsertViaExtension(deps model.Dependenci...
method HandleDeleteViaExtension (line 108) | func (h *LegacyHandler) HandleDeleteViaExtension(deps model.Dependenci...
function NewLegacyHandler (line 18) | func NewLegacyHandler(deps model.Dependencies) *LegacyHandler {
FILE: internal/http/handlers/legacy_test.go
function SetFakeAuthorizationHeader (line 18) | func SetFakeAuthorizationHeader(t *testing.T, deps model.Dependencies, c...
function TestLegacyHandler (line 24) | func TestLegacyHandler(t *testing.T) {
FILE: internal/http/handlers/swagger.go
function HandleSwagger (line 13) | func HandleSwagger(deps model.Dependencies, c model.WebContext) {
FILE: internal/http/handlers/swagger_test.go
function TestHandleSwagger (line 13) | func TestHandleSwagger(t *testing.T) {
FILE: internal/http/handlers/system.go
function HandleLiveness (line 11) | func HandleLiveness(deps model.Dependencies, c model.WebContext) {
FILE: internal/http/handlers/system_test.go
function TestHandleLiveness (line 11) | func TestHandleLiveness(t *testing.T) {
FILE: internal/http/http.go
function ToHTTPHandler (line 11) | func ToHTTPHandler(deps model.Dependencies, h model.HttpHandler, middlew...
FILE: internal/http/http_test.go
type testMiddleware (line 15) | type testMiddleware struct
method OnRequest (line 21) | func (m *testMiddleware) OnRequest(deps model.Dependencies, c model.We...
method OnResponse (line 29) | func (m *testMiddleware) OnResponse(deps model.Dependencies, c model.W...
function TestToHTTPHandler (line 37) | func TestToHTTPHandler(t *testing.T) {
FILE: internal/http/middleware/auth.go
type AuthMiddleware (line 15) | type AuthMiddleware struct
method OnRequest (line 23) | func (m *AuthMiddleware) OnRequest(deps model.Dependencies, c model.We...
method OnResponse (line 53) | func (m *AuthMiddleware) OnResponse(deps model.Dependencies, c model.W...
function NewAuthMiddleware (line 19) | func NewAuthMiddleware(deps model.Dependencies) *AuthMiddleware {
function RequireLoggedInUser (line 58) | func RequireLoggedInUser(deps model.Dependencies, c model.WebContext) er...
function RequireLoggedInAdmin (line 67) | func RequireLoggedInAdmin(deps model.Dependencies, c model.WebContext) e...
function getTokenFromHeader (line 82) | func getTokenFromHeader(r *http.Request) string {
function getTokenFromCookie (line 97) | func getTokenFromCookie(r *http.Request) string {
FILE: internal/http/middleware/auth_sso_proxy.go
type AuthSSOProxyMiddleware (line 13) | type AuthSSOProxyMiddleware struct
method OnRequest (line 38) | func (m *AuthSSOProxyMiddleware) OnRequest(deps model.Dependencies, c ...
method ssoAccount (line 60) | func (m *AuthSSOProxyMiddleware) ssoAccount(deps model.Dependencies, c...
method isTrustedIP (line 93) | func (m *AuthSSOProxyMiddleware) isTrustedIP(ip net.IP) bool {
method OnResponse (line 102) | func (m *AuthSSOProxyMiddleware) OnResponse(deps model.Dependencies, c...
function NewAuthSSOProxyMiddleware (line 19) | func NewAuthSSOProxyMiddleware(deps model.Dependencies) *AuthSSOProxyMid...
FILE: internal/http/middleware/auth_sso_proxy_test.go
function TestAuthMiddlewareWithSSO (line 16) | func TestAuthMiddlewareWithSSO(t *testing.T) {
FILE: internal/http/middleware/auth_test.go
function TestAuthMiddleware (line 17) | func TestAuthMiddleware(t *testing.T) {
function TestRequireLoggedInUser (line 102) | func TestRequireLoggedInUser(t *testing.T) {
function TestRequireLoggedInAdmin (line 131) | func TestRequireLoggedInAdmin(t *testing.T) {
FILE: internal/http/middleware/cors.go
type CORSMiddleware (line 9) | type CORSMiddleware struct
method OnRequest (line 13) | func (m *CORSMiddleware) OnRequest(deps model.Dependencies, c model.We...
method OnResponse (line 20) | func (m *CORSMiddleware) OnResponse(deps model.Dependencies, c model.W...
function NewCORSMiddleware (line 27) | func NewCORSMiddleware(allowedOrigins []string) *CORSMiddleware {
FILE: internal/http/middleware/cors_test.go
function TestCORSMiddleware (line 14) | func TestCORSMiddleware(t *testing.T) {
FILE: internal/http/middleware/logging.go
type LoggingMiddleware (line 13) | type LoggingMiddleware struct
method OnRequest (line 17) | func (m *LoggingMiddleware) OnRequest(deps model.Dependencies, c model...
method OnResponse (line 22) | func (m *LoggingMiddleware) OnResponse(deps model.Dependencies, c mode...
function NewLoggingMiddleware (line 32) | func NewLoggingMiddleware() *LoggingMiddleware {
FILE: internal/http/middleware/message_response.go
type responseMiddlewareBody (line 12) | type responseMiddlewareBody struct
type MessageResponseMiddleware (line 17) | type MessageResponseMiddleware struct
method OnRequest (line 21) | func (m *MessageResponseMiddleware) OnRequest(deps model.Dependencies,...
method OnResponse (line 32) | func (m *MessageResponseMiddleware) OnResponse(deps model.Dependencies...
function NewMessageResponseMiddleware (line 83) | func NewMessageResponseMiddleware(deps model.Dependencies) *MessageRespo...
type responseRecorder (line 88) | type responseRecorder struct
method Header (line 104) | func (r *responseRecorder) Header() http.Header {
method WriteHeader (line 108) | func (r *responseRecorder) WriteHeader(statusCode int) {
method Write (line 112) | func (r *responseRecorder) Write(b []byte) (int, error) {
function newResponseRecorder (line 95) | func newResponseRecorder(original http.ResponseWriter) *responseRecorder {
FILE: internal/http/middleware/message_response_test.go
function TestMessageResponseMiddleware (line 16) | func TestMessageResponseMiddleware(t *testing.T) {
FILE: internal/http/middleware/request_id.go
constant RequestIDHeader (line 10) | RequestIDHeader = "X-Request-ID"
type RequestIDMiddleware (line 14) | type RequestIDMiddleware struct
method OnRequest (line 24) | func (m *RequestIDMiddleware) OnRequest(deps model.Dependencies, c mod...
method OnResponse (line 42) | func (m *RequestIDMiddleware) OnResponse(deps model.Dependencies, c mo...
function NewRequestIDMiddleware (line 19) | func NewRequestIDMiddleware(deps model.Dependencies) *RequestIDMiddleware {
FILE: internal/http/middleware/request_id_test.go
function TestRequestIDMiddleware (line 12) | func TestRequestIDMiddleware(t *testing.T) {
FILE: internal/http/response/file.go
type SendFileOptions (line 14) | type SendFileOptions struct
function SendFile (line 19) | func SendFile(c model.WebContext, storage model.StorageDomain, path stri...
FILE: internal/http/response/file_test.go
function newMockStorage (line 19) | func newMockStorage(deps model.Dependencies, fs afero.Fs) model.StorageD...
function TestSendFile (line 23) | func TestSendFile(t *testing.T) {
type errorFs (line 132) | type errorFs struct
method Open (line 137) | func (e *errorFs) Open(name string) (afero.File, error) {
FILE: internal/http/response/response.go
type Response (line 10) | type Response struct
method GetData (line 19) | func (r *Response) GetData() any {
method IsError (line 24) | func (r *Response) IsError() bool {
method Send (line 29) | func (r *Response) Send(c model.WebContext, contentType string) error {
method SendJSON (line 37) | func (r *Response) SendJSON(c model.WebContext) error {
function NewResponse (line 44) | func NewResponse(message any, statusCode int) *Response {
FILE: internal/http/response/response_test.go
function TestNewResponse (line 13) | func TestNewResponse(t *testing.T) {
function TestResponse_IsError (line 46) | func TestResponse_IsError(t *testing.T) {
function TestResponse_GetMessage (line 71) | func TestResponse_GetMessage(t *testing.T) {
function TestResponse_Send (line 96) | func TestResponse_Send(t *testing.T) {
FILE: internal/http/response/shortcuts.go
constant internalServerErrorMessage (line 11) | internalServerErrorMessage = "Internal server error, please contact an a...
function New (line 14) | func New(statusCode int, data any) *Response {
function Send (line 19) | func Send(c model.WebContext, statusCode int, message any, contentType s...
function SendError (line 24) | func SendError(c model.WebContext, statusCode int, message any) error {
function SendErrorWithParams (line 32) | func SendErrorWithParams(c model.WebContext, statusCode int, data any, e...
function SendInternalServerError (line 37) | func SendInternalServerError(c model.WebContext) error {
function RedirectToLogin (line 42) | func RedirectToLogin(c model.WebContext, webroot, dst string) {
function NotFound (line 53) | func NotFound(c model.WebContext) {
function SendJSON (line 58) | func SendJSON(c model.WebContext, statusCode int, data any) error {
function SendTemplate (line 64) | func SendTemplate(c model.WebContext, name string, data any) error {
FILE: internal/http/response/shortcuts_test.go
function TestNew (line 14) | func TestNew(t *testing.T) {
function TestSend (line 28) | func TestSend(t *testing.T) {
function TestSendError (line 54) | func TestSendError(t *testing.T) {
function TestSendInternalServerError (line 87) | func TestSendInternalServerError(t *testing.T) {
function TestRedirectToLogin (line 103) | func TestRedirectToLogin(t *testing.T) {
function TestNotFound (line 121) | func TestNotFound(t *testing.T) {
function TestSendJSON (line 129) | func TestSendJSON(t *testing.T) {
FILE: internal/http/server.go
type HttpServer (line 21) | type HttpServer struct
method Setup (line 27) | func (s *HttpServer) Setup(cfg *config.Config, deps *dependencies.Depe...
method Start (line 201) | func (s *HttpServer) Start(_ context.Context) error {
method Stop (line 211) | func (s *HttpServer) Stop(ctx context.Context) error {
method WaitStop (line 216) | func (s *HttpServer) WaitStop(ctx context.Context) {
function NewHttpServer (line 228) | func NewHttpServer(logger *logrus.Logger) *HttpServer {
FILE: internal/http/server_test.go
function TestNewHttpServer (line 19) | func TestNewHttpServer(t *testing.T) {
function TestHttpServer_Setup (line 26) | func TestHttpServer_Setup(t *testing.T) {
function TestHttpServer_StartStop (line 120) | func TestHttpServer_StartStop(t *testing.T) {
function TestHttpServer_Middleware (line 144) | func TestHttpServer_Middleware(t *testing.T) {
function TestHttpServer_APIEndpoints (line 203) | func TestHttpServer_APIEndpoints(t *testing.T) {
FILE: internal/http/templates/templates.go
constant leftTemplateDelim (line 14) | leftTemplateDelim = "$$"
constant rightTemplateDelim (line 15) | rightTemplateDelim = "$$"
function SetupTemplates (line 21) | func SetupTemplates(config *config.Config) error {
function RenderTemplate (line 43) | func RenderTemplate(w io.Writer, name string, data any) error {
FILE: internal/http/webcontext/auth.go
method UserIsLogged (line 10) | func (c *WebContext) UserIsLogged() bool {
method GetAccount (line 15) | func (c *WebContext) GetAccount() *model.AccountDTO {
method SetAccount (line 23) | func (c *WebContext) SetAccount(account *model.AccountDTO) {
function WithAccount (line 29) | func WithAccount(ctx context.Context, account *model.AccountDTO) context...
FILE: internal/http/webcontext/auth_test.go
function TestUserIsLogged (line 12) | func TestUserIsLogged(t *testing.T) {
function TestGetAccount (line 36) | func TestGetAccount(t *testing.T) {
function TestWithAccount (line 62) | func TestWithAccount(t *testing.T) {
FILE: internal/http/webcontext/context.go
type WebContext (line 9) | type WebContext struct
method Context (line 23) | func (c *WebContext) Context() context.Context {
method WithContext (line 28) | func (c *WebContext) WithContext(ctx context.Context) *WebContext {
method ResponseWriter (line 35) | func (c *WebContext) ResponseWriter() http.ResponseWriter {
method SetResponseWriter (line 40) | func (c *WebContext) SetResponseWriter(w http.ResponseWriter) {
method Request (line 44) | func (c *WebContext) Request() *http.Request {
method GetRequestID (line 49) | func (c *WebContext) GetRequestID() string {
method SetRequestID (line 57) | func (c *WebContext) SetRequestID(id string) {
function NewWebContext (line 15) | func NewWebContext(w http.ResponseWriter, r *http.Request) *WebContext {
FILE: internal/http/webcontext/keys.go
type contextKey (line 3) | type contextKey
constant accountKey (line 6) | accountKey contextKey = "account"
constant requestIDKey (line 7) | requestIDKey contextKey = "requestID"
FILE: internal/model/account.go
type Account (line 12) | type Account struct
method ToDTO (line 50) | func (a Account) ToDTO() AccountDTO {
type UserConfig (line 20) | type UserConfig struct
method Scan (line 32) | func (c *UserConfig) Scan(value interface{}) error {
method Value (line 45) | func (c UserConfig) Value() (driver.Value, error) {
type AccountDTO (line 63) | type AccountDTO struct
method IsOwner (line 71) | func (adto *AccountDTO) IsOwner() bool {
method IsValidCreate (line 75) | func (adto *AccountDTO) IsValidCreate() error {
method IsValidUpdate (line 87) | func (adto *AccountDTO) IsValidUpdate() error {
type JWTClaim (line 95) | type JWTClaim struct
FILE: internal/model/bookmark.go
type Bookmark (line 9) | type Bookmark struct
method ToDTO (line 59) | func (b *Bookmark) ToDTO() BookmarkDTO {
type BookmarkDTO (line 23) | type BookmarkDTO struct
method ToBookmark (line 44) | func (dto *BookmarkDTO) ToBookmark() Bookmark {
function GetThumbnailPath (line 75) | func GetThumbnailPath(bookmark *BookmarkDTO) string {
function GetEbookPath (line 80) | func GetEbookPath(bookmark *BookmarkDTO) string {
function GetArchivePath (line 85) | func GetArchivePath(bookmark *BookmarkDTO) string {
FILE: internal/model/bookmark_test.go
function TestBookmarkToDTO (line 10) | func TestBookmarkToDTO(t *testing.T) {
function TestBookmarkDTOToBookmark (line 49) | func TestBookmarkDTOToBookmark(t *testing.T) {
function TestGetThumbnailPath (line 89) | func TestGetThumbnailPath(t *testing.T) {
function TestGetEbookPath (line 121) | func TestGetEbookPath(t *testing.T) {
function TestGetArchivePath (line 153) | func TestGetArchivePath(t *testing.T) {
function TestBookmarkRoundTrip (line 185) | func TestBookmarkRoundTrip(t *testing.T) {
FILE: internal/model/const.go
constant DataDirPerm (line 4) | DataDirPerm = 0744
constant DatabaseDateFormat (line 7) | DatabaseDateFormat = "2006-01-02 15:04:05"
FILE: internal/model/database.go
type DBID (line 9) | type DBID
type DB (line 12) | type DB interface
type DBOrderMethod (line 107) | type DBOrderMethod
constant DefaultOrder (line 111) | DefaultOrder DBOrderMethod = iota
constant ByLastAdded (line 113) | ByLastAdded
constant ByLastModified (line 115) | ByLastModified
type DBGetBookmarksOptions (line 119) | type DBGetBookmarksOptions struct
type DBListAccountsOptions (line 131) | type DBListAccountsOptions struct
type DBTagOrderBy (line 142) | type DBTagOrderBy
constant DBTagOrderByTagName (line 145) | DBTagOrderByTagName DBTagOrderBy = "name"
type DBListTagsOptions (line 149) | type DBListTagsOptions struct
FILE: internal/model/dependencies.go
type Dependencies (line 9) | type Dependencies interface
type DomainDependencies (line 17) | type DomainDependencies interface
FILE: internal/model/domains.go
type BookmarksDomain (line 13) | type BookmarksDomain interface
type AuthDomain (line 26) | type AuthDomain interface
type AccountsDomain (line 32) | type AccountsDomain interface
type ArchiverDomain (line 40) | type ArchiverDomain interface
type StorageDomain (line 45) | type StorageDomain interface
type TagsDomain (line 54) | type TagsDomain interface
FILE: internal/model/http.go
constant ContextAccountKey (line 7) | ContextAccountKey = "account"
constant AuthorizationHeader (line 10) | AuthorizationHeader = "Authorization"
constant AuthorizationTokenType (line 12) | AuthorizationTokenType = "Bearer"
type WebContext (line 16) | type WebContext interface
type HttpHandler (line 28) | type HttpHandler
type HttpMiddleware (line 31) | type HttpMiddleware interface
FILE: internal/model/legacy.go
type LegacyLoginHandler (line 5) | type LegacyLoginHandler
FILE: internal/model/main.go
constant ShioriURLNamespace (line 12) | ShioriURLNamespace = "https://github.com/go-shiori/shiori"
FILE: internal/model/ptr.go
function Ptr (line 4) | func Ptr[t any](a t) *t {
FILE: internal/model/slices.go
function SliceDifference (line 5) | func SliceDifference[T comparable](haystack, needle []T) []T {
FILE: internal/model/slices_test.go
function TestSliceDifference (line 9) | func TestSliceDifference(t *testing.T) {
FILE: internal/model/tag.go
type BookmarkTag (line 8) | type BookmarkTag struct
type Tag (line 14) | type Tag struct
method ToDTO (line 26) | func (t *Tag) ToDTO() TagDTO {
type TagDTO (line 20) | type TagDTO struct
method ToTag (line 35) | func (t *TagDTO) ToTag() Tag {
type ListTagsOptions (line 43) | type ListTagsOptions struct
method IsValid (line 53) | func (o ListTagsOptions) IsValid() error {
FILE: internal/model/tag_test.go
function TestListTagsOptions_IsValid (line 9) | func TestListTagsOptions_IsValid(t *testing.T) {
FILE: internal/model/validation.go
type ValidationError (line 6) | type ValidationError struct
method Error (line 11) | func (v ValidationError) Error() string {
function NewValidationError (line 15) | func NewValidationError(field, message string) ValidationError {
FILE: internal/testutil/accounts.go
function NewAdminUser (line 14) | func NewAdminUser(deps model.Dependencies) (*model.AccountDTO, string, e...
FILE: internal/testutil/accounts_test.go
function TestNewAdminUser (line 11) | func TestNewAdminUser(t *testing.T) {
FILE: internal/testutil/http.go
function NewTestWebContext (line 15) | func NewTestWebContext() (model.WebContext, *httptest.ResponseRecorder) {
function NewTestWebContextWithMethod (line 22) | func NewTestWebContextWithMethod(method, path string, opts ...Option) (m...
function WithBody (line 32) | func WithBody(body string) Option {
function WithHeader (line 38) | func WithHeader(name, value string) Option {
function WithAuthToken (line 45) | func WithAuthToken(token string) Option {
function WithAccount (line 51) | func WithAccount(account *model.AccountDTO) Option {
function WithFakeAccount (line 58) | func WithFakeAccount(isAdmin bool) Option {
function WithRequestPathValue (line 65) | func WithRequestPathValue(key, value string) Option {
function WithRequestQueryParam (line 72) | func WithRequestQueryParam(key, value string) Option {
function PerformRequest (line 81) | func PerformRequest(deps model.Dependencies, handler model.HttpHandler, ...
function PerformRequestOnRecorder (line 95) | func PerformRequestOnRecorder(deps model.Dependencies, w *httptest.Respo...
function FakeAccount (line 105) | func FakeAccount(isAdmin bool) *model.AccountDTO {
function SetFakeUser (line 114) | func SetFakeUser(c model.WebContext) {
function SetFakeAdmin (line 123) | func SetFakeAdmin(c model.WebContext) {
function WithFakeUser (line 132) | func WithFakeUser() Option {
function WithFakeAdmin (line 137) | func WithFakeAdmin() Option {
function SetRequestPathValue (line 142) | func SetRequestPathValue(c model.WebContext, key, value string) {
FILE: internal/testutil/response.go
type testResponse (line 12) | type testResponse struct
method AssertMessageIsEmptyList (line 16) | func (r *testResponse) AssertMessageIsEmptyList(t *testing.T) {
method AssertMessageIsNotEmptyList (line 23) | func (r *testResponse) AssertMessageIsNotEmptyList(t *testing.T) {
method AssertMessageIsListLength (line 30) | func (r *testResponse) AssertMessageIsListLength(t *testing.T, length ...
method ForEach (line 39) | func (r *testResponse) ForEach(t *testing.T, fn func(item map[string]a...
method AssertNilMessage (line 48) | func (r *testResponse) AssertNilMessage(t *testing.T) {
method AssertMessageEquals (line 52) | func (r testResponse) AssertMessageEquals(t *testing.T, expected any) {
method AssertMessageJSONContains (line 56) | func (r testResponse) AssertMessageJSONContains(t *testing.T, expected...
method AssertMessageJSONContainsKey (line 63) | func (r testResponse) AssertMessageJSONContainsKey(t *testing.T, key s...
method AssertMessageJSONKeyValue (line 74) | func (r *testResponse) AssertMessageJSONKeyValue(t *testing.T, key str...
method AssertMessageContains (line 79) | func (r *testResponse) AssertMessageContains(t *testing.T, expected st...
method AssertMessageIsBytes (line 83) | func (r *testResponse) AssertMessageIsBytes(t *testing.T, expected []b...
method AssertOk (line 87) | func (r *testResponse) AssertOk(t *testing.T) {
method AssertNotOk (line 91) | func (r *testResponse) AssertNotOk(t *testing.T) {
function NewTestResponseFromRecorder (line 95) | func NewTestResponseFromRecorder(w *httptest.ResponseRecorder) *testResp...
FILE: internal/testutil/shiori.go
function GetTestConfigurationAndDependencies (line 19) | func GetTestConfigurationAndDependencies(t *testing.T, ctx context.Conte...
function GetValidBookmark (line 51) | func GetValidBookmark() *model.BookmarkDTO {
function GetValidAccount (line 62) | func GetValidAccount() *model.Account {
FILE: internal/view/assets/js/component/bookmark.js
method default (line 65) | default() {
method mainURL (line 71) | mainURL() {
method ebookURL (line 80) | ebookURL() {
method hostnameURL (line 87) | hostnameURL() {
method thumbnailVisible (line 91) | thumbnailVisible() {
method excerptVisible (line 94) | excerptVisible() {
method thumbnailStyleURL (line 97) | thumbnailStyleURL() {
method eventItem (line 102) | eventItem() {
method tagClicked (line 110) | tagClicked(name, event) {
method selectBookmark (line 113) | selectBookmark() {
method editBookmark (line 116) | editBookmark() {
method deleteBookmark (line 119) | deleteBookmark() {
method updateBookmark (line 122) | updateBookmark() {
method downloadebook (line 125) | downloadebook() {
FILE: internal/view/assets/js/component/dialog.js
method default (line 76) | default() {
method default (line 91) | default() {
method default (line 97) | default() {
method default (line 103) | default() {
method data (line 108) | data() {
method btnTabIndex (line 114) | btnTabIndex() {
method handler (line 121) | handler() {
method "fields.length" (line 148) | "fields.length"() {
method handler (line 153) | handler() {
method fieldType (line 159) | fieldType(f) {
method handleMainClick (line 164) | handleMainClick() {
method handleSecondClick (line 176) | handleSecondClick() {
method handleEscPressed (line 179) | handleEscPressed() {
method handleInput (line 182) | handleInput(index) {
method handleInputEnter (line 216) | handleInputEnter(index) {
method focus (line 235) | focus() {
FILE: internal/view/assets/js/component/login.js
method data (line 37) | data() {
method sanitizeDestination (line 49) | sanitizeDestination(dst) {
method parseJWT (line 73) | parseJWT(token) {
method login (line 81) | async login() {
method checkSession (line 139) | async checkSession() {
method mounted (line 151) | async mounted() {
FILE: internal/view/assets/js/component/pagination.js
method changePage (line 36) | changePage(page) {
FILE: internal/view/assets/js/page/base.js
method default (line 7) | default() {
method default (line 17) | default() {
method data (line 31) | data() {
method defaultDialog (line 37) | defaultDialog() {
method showDialog (line 58) | showDialog(opt) {
method getErrorMessage (line 64) | async getErrorMessage(err) {
method isSessionError (line 82) | isSessionError(err) {
method themeSwitch (line 97) | themeSwitch(theme) {
method showErrorDialog (line 114) | showErrorDialog(msg) {
method saveSetting (line 127) | async saveSetting(key, value) {
method logout (line 137) | async logout() {
FILE: internal/view/assets/js/page/home.js
method data (line 107) | data() {
method listIsEmpty (line 143) | listIsEmpty() {
method "dialogTags.editMode" (line 148) | "dialogTags.editMode"(editMode) {
method clearHomePage (line 161) | clearHomePage() {
method reloadData (line 165) | reloadData() {
method loadData (line 171) | async loadData(saveState, fetchTags) {
method searchBookmarks (line 266) | searchBookmarks() {
method changePage (line 270) | changePage(page) {
method toggleEditMode (line 275) | toggleEditMode() {
method toggleSelection (line 279) | toggleSelection(item) {
method isSelected (line 284) | isSelected(bookId) {
method dialogTagClicked (line 287) | dialogTagClicked(event, tag) {
method bookmarkTagClicked (line 295) | bookmarkTagClicked(event, tagName) {
method filterTag (line 298) | filterTag(tagName, excludeMode) {
method showDialogAdd (line 353) | showDialogAdd(values) {
method showDialogEdit (line 453) | showDialogEdit(item) {
method showDialogDelete (line 545) | showDialogDelete(items) {
method ebookGenerate (line 605) | ebookGenerate(items) {
method showDialogUpdateCache (line 673) | showDialogUpdateCache(items) {
method showDialogAddTags (line 778) | showDialogAddTags(items) {
method showDialogTags (line 854) | showDialogTags() {
method showDialogRenameTag (line 861) | showDialogRenameTag(tag) {
method mounted (line 931) | mounted() {
FILE: internal/view/assets/js/page/setting.js
method data (line 111) | data() {
method saveSetting (line 119) | saveSetting() {
method loadAccounts (line 164) | async loadAccounts() {
method loadSystemInfo (line 179) | async loadSystemInfo() {
method showDialogNewAccount (line 191) | showDialogNewAccount() {
method showDialogChangePassword (line 268) | showDialogChangePassword(account) {
method showDialogDeleteAccount (line 357) | showDialogDeleteAccount(account, idx) {
method mounted (line 381) | mounted() {
FILE: internal/view/assets/js/url.js
function urlConfig (line 32) | function urlConfig(url) {
function getCurrUrl (line 93) | function getCurrUrl() {
function parse (line 107) | function parse(self, url, absolutize) {
function encode (line 186) | function encode(s) {
function decode (line 190) | function decode(s) {
function QueryString (line 232) | function QueryString(qs) {
function Url (line 310) | function Url(url, noTransform) {
FILE: internal/view/assets/js/utils/api.js
function handleApiResponse (line 2) | async function handleApiResponse(response) {
function handleApiError (line 30) | async function handleApiError(error) {
function apiRequest (line 52) | async function apiRequest(url, options = {}) {
FILE: internal/view/assets/js/vue.js
function isUndef (line 18) | function isUndef (v) {
function isDef (line 22) | function isDef (v) {
function isTrue (line 26) | function isTrue (v) {
function isFalse (line 30) | function isFalse (v) {
function isPrimitive (line 37) | function isPrimitive (value) {
function isObject (line 52) | function isObject (obj) {
function toRawType (line 61) | function toRawType (value) {
function isPlainObject (line 69) | function isPlainObject (obj) {
function isRegExp (line 73) | function isRegExp (v) {
function isValidArrayIndex (line 80) | function isValidArrayIndex (val) {
function isPromise (line 85) | function isPromise (val) {
function toString (line 96) | function toString (val) {
function toNumber (line 108) | function toNumber (val) {
function makeMap (line 117) | function makeMap (
function remove (line 144) | function remove (arr, item) {
function hasOwn (line 157) | function hasOwn (obj, key) {
function cached (line 164) | function cached (fn) {
function polyfillBind (line 204) | function polyfillBind (fn, ctx) {
function nativeBind (line 218) | function nativeBind (fn, ctx) {
function toArray (line 229) | function toArray (list, start) {
function extend (line 242) | function extend (to, _from) {
function toObject (line 252) | function toObject (arr) {
function noop (line 269) | function noop (a, b, c) {}
function genStaticKeys (line 286) | function genStaticKeys (modules) {
function looseEqual (line 296) | function looseEqual (a, b) {
function looseIndexOf (line 336) | function looseIndexOf (arr, val) {
function once (line 346) | function once (fn) {
function isReserved (line 489) | function isReserved (str) {
function def (line 497) | function def (obj, key, val, enumerable) {
function parsePath (line 510) | function parsePath (path) {
function isNative (line 581) | function isNative (Ctor) {
function Set (line 597) | function Set () {
function pushTarget (line 755) | function pushTarget (target) {
function popTarget (line 760) | function popTarget () {
function createTextVNode (line 821) | function createTextVNode (val) {
function cloneVNode (line 829) | function cloneVNode (vnode) {
function toggleObserving (line 912) | function toggleObserving (value) {
function protoAugment (line 966) | function protoAugment (target, src) {
function copyAugment (line 977) | function copyAugment (target, src, keys) {
function observe (line 989) | function observe (value, asRootData) {
function defineReactive$$1 (line 1014) | function defineReactive$$1 (
function set (line 1080) | function set (target, key, val) {
function del (line 1114) | function del (target, key) {
function dependArray (line 1145) | function dependArray (value) {
function mergeData (line 1182) | function mergeData (to, from) {
function mergeDataOrFn (line 1212) | function mergeDataOrFn (
function mergeHook (line 1279) | function mergeHook (
function dedupeHooks (line 1295) | function dedupeHooks (hooks) {
function mergeAssets (line 1316) | function mergeAssets (
function checkComponents (line 1406) | function checkComponents (options) {
function validateComponentName (line 1412) | function validateComponentName (name) {
function normalizeProps (line 1431) | function normalizeProps (options, vm) {
function normalizeInject (line 1468) | function normalizeInject (options, vm) {
function normalizeDirectives (line 1495) | function normalizeDirectives (options) {
function assertObjectType (line 1507) | function assertObjectType (name, value, vm) {
function mergeOptions (line 1521) | function mergeOptions (
function resolveAsset (line 1575) | function resolveAsset (
function validateProp (line 1607) | function validateProp (
function getPropDefaultValue (line 1649) | function getPropDefaultValue (vm, prop, key) {
function assertProp (line 1682) | function assertProp (
function assertType (line 1733) | function assertType (value, type) {
function getType (line 1761) | function getType (fn) {
function isSameType (line 1766) | function isSameType (a, b) {
function getTypeIndex (line 1770) | function getTypeIndex (type, expectedTypes) {
function getInvalidTypeMessage (line 1782) | function getInvalidTypeMessage (name, value, expectedTypes) {
function styleValue (line 1803) | function styleValue (value, type) {
function isExplicable (line 1813) | function isExplicable (value) {
function isBoolean (line 1818) | function isBoolean () {
function handleError (line 1827) | function handleError (err, vm, info) {
function invokeWithErrorHandling (line 1854) | function invokeWithErrorHandling (
function globalHandleError (line 1875) | function globalHandleError (err, vm, info) {
function logError (line 1890) | function logError (err, vm, info) {
function flushCallbacks (line 1909) | function flushCallbacks () {
function nextTick (line 1983) | function nextTick (cb, ctx) {
function traverse (line 2130) | function traverse (val) {
function _traverse (line 2135) | function _traverse (val, seen) {
function createFnInvoker (line 2175) | function createFnInvoker (fns, vm) {
function updateListeners (line 2194) | function updateListeners (
function mergeVNodeHook (line 2235) | function mergeVNodeHook (def, hookKey, hook) {
function extractPropsFromVNodeData (line 2270) | function extractPropsFromVNodeData (
function checkProp (line 2311) | function checkProp (
function simpleNormalizeChildren (line 2350) | function simpleNormalizeChildren (children) {
function normalizeChildren (line 2363) | function normalizeChildren (children) {
function isTextNode (line 2371) | function isTextNode (node) {
function normalizeArrayChildren (line 2375) | function normalizeArrayChildren (children, nestedIndex) {
function initProvide (line 2425) | function initProvide (vm) {
function initInjections (line 2434) | function initInjections (vm) {
function resolveInject (line 2455) | function resolveInject (inject, vm) {
function resolveSlots (line 2498) | function resolveSlots (
function isWhitespace (line 2538) | function isWhitespace (node) {
function normalizeScopedSlots (line 2544) | function normalizeScopedSlots (
function normalizeScopedSlot (line 2591) | function normalizeScopedSlot(normalSlots, key, fn) {
function proxyNormalSlot (line 2614) | function proxyNormalSlot(slots, key) {
function renderList (line 2623) | function renderList (
function renderSlot (line 2668) | function renderSlot (
function resolveFilter (line 2705) | function resolveFilter (id) {
function isKeyNotMatch (line 2711) | function isKeyNotMatch (expect, actual) {
function checkKeyCodes (line 2724) | function checkKeyCodes (
function bindObjectProps (line 2746) | function bindObjectProps (
function renderStatic (line 2801) | function renderStatic (
function markOnce (line 2826) | function markOnce (
function markStatic (line 2835) | function markStatic (
function markStaticNode (line 2851) | function markStaticNode (node, key, isOnce) {
function bindObjectListeners (line 2859) | function bindObjectListeners (data, value) {
function resolveScopedSlots (line 2880) | function resolveScopedSlots (
function bindDynamicKeys (line 2908) | function bindDynamicKeys (baseObj, values) {
function prependModifier (line 2927) | function prependModifier (value, symbol) {
function installRenderHelpers (line 2933) | function installRenderHelpers (target) {
function FunctionalRenderContext (line 2955) | function FunctionalRenderContext (
function createFunctionalComponent (line 3031) | function createFunctionalComponent (
function cloneAndMarkFunctionalResult (line 3072) | function cloneAndMarkFunctionalResult (vnode, data, contextVm, options, ...
function mergeProps (line 3088) | function mergeProps (to, from) {
function createComponent (line 3169) | function createComponent (
function createComponentInstanceForVnode (line 3268) | function createComponentInstanceForVnode (
function installComponentHooks (line 3286) | function installComponentHooks (data) {
function mergeHook$1 (line 3298) | function mergeHook$1 (f1, f2) {
function transformModel (line 3310) | function transformModel (options, data) {
function createElement (line 3337) | function createElement (
function _createElement (line 3356) | function _createElement (
function applyNS (line 3440) | function applyNS (vnode, ns, force) {
function registerDeepBindings (line 3461) | function registerDeepBindings (data) {
function initRender (line 3472) | function initRender (vm) {
function renderMixin (line 3506) | function renderMixin (Vue) {
function ensureCtor (line 3580) | function ensureCtor (comp, base) {
function createAsyncPlaceholder (line 3592) | function createAsyncPlaceholder (
function resolveAsyncComponent (line 3605) | function resolveAsyncComponent (
function isAsyncPlaceholder (line 3717) | function isAsyncPlaceholder (node) {
function getFirstComponentChild (line 3723) | function getFirstComponentChild (children) {
function initEvents (line 3738) | function initEvents (vm) {
function add (line 3750) | function add (event, fn) {
function remove$1 (line 3754) | function remove$1 (event, fn) {
function createOnceHandler (line 3758) | function createOnceHandler (event, fn) {
function updateComponentListeners (line 3768) | function updateComponentListeners (
function eventsMixin (line 3778) | function eventsMixin (Vue) {
function setActiveInstance (line 3876) | function setActiveInstance(vm) {
function initLifecycle (line 3884) | function initLifecycle (vm) {
function lifecycleMixin (line 3910) | function lifecycleMixin (Vue) {
function mountComponent (line 3993) | function mountComponent (
function updateChildComponent (line 4067) | function updateChildComponent (
function isInInactiveTree (line 4147) | function isInInactiveTree (vm) {
function activateChildComponent (line 4154) | function activateChildComponent (vm, direct) {
function deactivateChildComponent (line 4172) | function deactivateChildComponent (vm, direct) {
function callHook (line 4188) | function callHook (vm, hook) {
function resetSchedulerState (line 4219) | function resetSchedulerState () {
function flushSchedulerQueue (line 4252) | function flushSchedulerQueue () {
function callUpdatedHooks (line 4311) | function callUpdatedHooks (queue) {
function queueActivatedComponent (line 4326) | function queueActivatedComponent (vm) {
function callActivatedHooks (line 4333) | function callActivatedHooks (queue) {
function queueWatcher (line 4345) | function queueWatcher (watcher) {
function proxy (line 4590) | function proxy (target, sourceKey, key) {
function initState (line 4600) | function initState (vm) {
function initProps (line 4616) | function initProps (vm, propsOptions) {
function initData (line 4664) | function initData (vm) {
function getData (line 4706) | function getData (data, vm) {
function initComputed (line 4721) | function initComputed (vm, computed) {
function defineComputed (line 4762) | function defineComputed (
function createComputedGetter (line 4792) | function createComputedGetter (key) {
function createGetterInvoker (line 4807) | function createGetterInvoker(fn) {
function initMethods (line 4813) | function initMethods (vm, methods) {
function initWatch (line 4841) | function initWatch (vm, watch) {
function createWatcher (line 4854) | function createWatcher (
function stateMixin (line 4870) | function stateMixin (Vue) {
function initMixin (line 4925) | function initMixin (Vue) {
function initInternalComponent (line 4982) | function initInternalComponent (vm, options) {
function resolveConstructorOptions (line 5001) | function resolveConstructorOptions (Ctor) {
function resolveModifiedOptions (line 5025) | function resolveModifiedOptions (Ctor) {
function Vue (line 5038) | function Vue (options) {
function initUse (line 5054) | function initUse (Vue) {
function initMixin$1 (line 5076) | function initMixin$1 (Vue) {
function initExtend (line 5085) | function initExtend (Vue) {
function initProps$1 (line 5161) | function initProps$1 (Comp) {
function initComputed$1 (line 5168) | function initComputed$1 (Comp) {
function initAssetRegisters (line 5177) | function initAssetRegisters (Vue) {
function getComponentName (line 5211) | function getComponentName (opts) {
function matches (line 5215) | function matches (pattern, name) {
function pruneCache (line 5227) | function pruneCache (keepAliveInstance, filter) {
function pruneCacheEntry (line 5242) | function pruneCacheEntry (
function initGlobalAPI (line 5343) | function initGlobalAPI (Vue) {
function genClassForVnode (line 5468) | function genClassForVnode (vnode) {
function mergeClassData (line 5486) | function mergeClassData (child, parent) {
function renderClass (line 5495) | function renderClass (
function concat (line 5506) | function concat (a, b) {
function stringifyClass (line 5510) | function stringifyClass (value) {
function stringifyArray (line 5524) | function stringifyArray (value) {
function stringifyObject (line 5536) | function stringifyObject (value) {
function getTagNamespace (line 5583) | function getTagNamespace (tag) {
function isUnknownElement (line 5595) | function isUnknownElement (tag) {
function query (line 5627) | function query (el) {
function createElement$1 (line 5644) | function createElement$1 (tagName, vnode) {
function createElementNS (line 5656) | function createElementNS (namespace, tagName) {
function createTextNode (line 5660) | function createTextNode (text) {
function createComment (line 5664) | function createComment (text) {
function insertBefore (line 5668) | function insertBefore (parentNode, newNode, referenceNode) {
function removeChild (line 5672) | function removeChild (node, child) {
function appendChild (line 5676) | function appendChild (node, child) {
function parentNode (line 5680) | function parentNode (node) {
function nextSibling (line 5684) | function nextSibling (node) {
function tagName (line 5688) | function tagName (node) {
function setTextContent (line 5692) | function setTextContent (node, text) {
function setStyleScope (line 5696) | function setStyleScope (node, scopeId) {
function registerRef (line 5732) | function registerRef (vnode, isRemoval) {
function sameVnode (line 5775) | function sameVnode (a, b) {
function sameInputType (line 5792) | function sameInputType (a, b) {
function createKeyToOldIdx (line 5800) | function createKeyToOldIdx (children, beginIdx, endIdx) {
function createPatchFunction (line 5810) | function createPatchFunction (backend) {
function updateDirectives (line 6538) | function updateDirectives (oldVnode, vnode) {
function _update (line 6544) | function _update (oldVnode, vnode) {
function normalizeDirectives$1 (line 6607) | function normalizeDirectives$1 (
function getRawDirName (line 6630) | function getRawDirName (dir) {
function callHook$1 (line 6634) | function callHook$1 (dir, hook, vnode, oldVnode, isDestroy) {
function updateAttrs (line 6652) | function updateAttrs (oldVnode, vnode) {
function setAttr (line 6693) | function setAttr (el, key, value) {
function baseSetAttr (line 6722) | function baseSetAttr (el, key, value) {
function updateClass (line 6754) | function updateClass (oldVnode, vnode) {
function parseFilters (line 6794) | function parseFilters (exp) {
function wrapFilter (line 6876) | function wrapFilter (exp, filter) {
function baseWarn (line 6893) | function baseWarn (msg, range) {
function pluckModuleFunction (line 6898) | function pluckModuleFunction (
function addProp (line 6907) | function addProp (el, name, value, range, dynamic) {
function addAttr (line 6912) | function addAttr (el, name, value, range, dynamic) {
function addRawAttr (line 6921) | function addRawAttr (el, name, value, range) {
function addDirective (line 6926) | function addDirective (
function prependModifierMarker (line 6947) | function prependModifierMarker (symbol, name, dynamic) {
function addHandler (line 6953) | function addHandler (
function getRawBindingAttr (line 7036) | function getRawBindingAttr (
function getBindingAttr (line 7045) | function getBindingAttr (
function getAndRemoveAttr (line 7067) | function getAndRemoveAttr (
function getAndRemoveAttrByRegex (line 7088) | function getAndRemoveAttrByRegex (
function rangeSetItem (line 7102) | function rangeSetItem (
function genComponentModel (line 7122) | function genComponentModel (
function genAssignmentCode (line 7154) | function genAssignmentCode (
function parseModel (line 7185) | function parseModel (val) {
function next (line 7225) | function next () {
function eof (line 7229) | function eof () {
function isStringStart (line 7233) | function isStringStart (chr) {
function parseBracket (line 7237) | function parseBracket (chr) {
function parseString (line 7255) | function parseString (chr) {
function model (line 7274) | function model (
function genCheckboxModel (line 7327) | function genCheckboxModel (
function genRadioModel (line 7358) | function genRadioModel (
function genSelect (line 7370) | function genSelect (
function genDefaultModel (line 7387) | function genDefaultModel (
function normalizeEvents (line 7446) | function normalizeEvents (on) {
function createOnceHandler$1 (line 7465) | function createOnceHandler$1 (event, handler, capture) {
function add$1 (line 7480) | function add$1 (
function remove$2 (line 7523) | function remove$2 (
function updateDOMListeners (line 7536) | function updateDOMListeners (oldVnode, vnode) {
function updateDOMProps (line 7557) | function updateDOMProps (oldVnode, vnode) {
function shouldUpdateValue (line 7629) | function shouldUpdateValue (elm, checkVal) {
function isNotInFocusAndDirty (line 7637) | function isNotInFocusAndDirty (elm, checkVal) {
function isDirtyWithModifiers (line 7647) | function isDirtyWithModifiers (elm, newVal) {
function normalizeStyleData (line 7682) | function normalizeStyleData (data) {
function normalizeStyleBinding (line 7692) | function normalizeStyleBinding (bindingStyle) {
function getStyle (line 7706) | function getStyle (vnode, checkChild) {
function updateStyle (line 7779) | function updateStyle (oldVnode, vnode) {
function addClass (line 7835) | function addClass (el, cls) {
function removeClass (line 7860) | function removeClass (el, cls) {
function resolveTransition (line 7893) | function resolveTransition (def$$1) {
function nextFrame (line 7953) | function nextFrame (fn) {
function addTransitionClass (line 7959) | function addTransitionClass (el, cls) {
function removeTransitionClass (line 7967) | function removeTransitionClass (el, cls) {
function whenTransitionEnds (line 7974) | function whenTransitionEnds (
function getTransitionInfo (line 8007) | function getTransitionInfo (el, expectedType) {
function getTimeout (line 8057) | function getTimeout (delays, durations) {
function toMs (line 8072) | function toMs (s) {
function enter (line 8078) | function enter (vnode, toggleDisplay) {
function leave (line 8229) | function leave (vnode, rm) {
function checkDuration (line 8334) | function checkDuration (val, name, vnode) {
function isValidDuration (line 8350) | function isValidDuration (val) {
function getHookArgumentsLength (line 8360) | function getHookArgumentsLength (fn) {
function _enter (line 8377) | function _enter (_, vnode) {
function setSelected (line 8482) | function setSelected (el, binding, vm) {
function actuallySetSelected (line 8492) | function actuallySetSelected (el, binding, vm) {
function hasNoMatchingOption (line 8525) | function hasNoMatchingOption (value, options) {
function getValue (line 8529) | function getValue (option) {
function onCompositionStart (line 8535) | function onCompositionStart (e) {
function onCompositionEnd (line 8539) | function onCompositionEnd (e) {
function trigger (line 8546) | function trigger (el, type) {
function locateNode (line 8555) | function locateNode (vnode) {
function getRealChild (line 8643) | function getRealChild (vnode) {
function extractTransitionData (line 8652) | function extractTransitionData (comp) {
function placeholder (line 8668) | function placeholder (h, rawChild) {
function hasParentTransition (line 8676) | function hasParentTransition (vnode) {
function isSameChild (line 8684) | function isSameChild (child, oldChild) {
function callPendingCbs (line 8951) | function callPendingCbs (c) {
function recordPosition (line 8962) | function recordPosition (c) {
function applyTranslation (line 8966) | function applyTranslation (c) {
function parseText (line 9048) | function parseText (
function transformNode (line 9085) | function transformNode (el, options) {
function genData (line 9109) | function genData (el) {
function transformNode$1 (line 9128) | function transformNode$1 (el, options) {
function genData$1 (line 9154) | function genData$1 (el) {
function decodeAttr (line 9243) | function decodeAttr (value, shouldDecodeNewlines) {
function parseHTML (line 9248) | function parseHTML (html, options) {
function createASTElement (line 9536) | function createASTElement (
function parse (line 9555) | function parse (
function processPre (line 9878) | function processPre (el) {
function processRawAttrs (line 9884) | function processRawAttrs (el) {
function processElement (line 9905) | function processElement (
function processKey (line 9930) | function processKey (el) {
function processRef (line 9957) | function processRef (el) {
function processFor (line 9965) | function processFor (el) {
function parseFor (line 9982) | function parseFor (exp) {
function processIf (line 10001) | function processIf (el) {
function processIfConditions (line 10020) | function processIfConditions (el, parent) {
function findPrevElement (line 10036) | function findPrevElement (children) {
function addIfCondition (line 10054) | function addIfCondition (el, condition) {
function processOnce (line 10061) | function processOnce (el) {
function processSlotContent (line 10070) | function processSlotContent (el) {
function getSlotName (line 10189) | function getSlotName (binding) {
function processSlotOutlet (line 10209) | function processSlotOutlet (el) {
function processComponent (line 10223) | function processComponent (el) {
function processAttrs (line 10233) | function processAttrs (el) {
function checkInFor (line 10366) | function checkInFor (el) {
function parseModifiers (line 10377) | function parseModifiers (name) {
function makeAttrsMap (line 10386) | function makeAttrsMap (attrs) {
function isTextTag (line 10400) | function isTextTag (el) {
function isForbiddenTag (line 10404) | function isForbiddenTag (el) {
function guardIESVGBug (line 10418) | function guardIESVGBug (attrs) {
function checkForAliasModel (line 10430) | function checkForAliasModel (el, value) {
function preTransformNode (line 10449) | function preTransformNode (el, options) {
function cloneASTElement (line 10511) | function cloneASTElement (el) {
function text (line 10527) | function text (el, dir) {
function html (line 10535) | function html (el, dir) {
function optimize (line 10580) | function optimize (root, options) {
function genStaticKeys$1 (line 10590) | function genStaticKeys$1 (keys) {
function markStatic$1 (line 10597) | function markStatic$1 (node) {
function markStaticRoots (line 10629) | function markStaticRoots (node, isInFor) {
function isStatic (line 10659) | function isStatic (node) {
function isDirectChildOfTemplateFor (line 10676) | function isDirectChildOfTemplateFor (node) {
function genHandlers (line 10743) | function genHandlers (
function genHandler (line 10766) | function genHandler (handler) {
function genKeyFilter (line 10825) | function genKeyFilter (keys) {
function genFilterCode (line 10835) | function genFilterCode (key) {
function on (line 10854) | function on (el, dir) {
function bind$1 (line 10863) | function bind$1 (el, dir) {
function generate (line 10898) | function generate (
function genElement (line 10910) | function genElement (el, state) {
function genStatic (line 10950) | function genStatic (el, state) {
function genOnce (line 10965) | function genOnce (el, state) {
function genIf (line 10992) | function genIf (
function genIfConditions (line 11002) | function genIfConditions (
function genFor (line 11029) | function genFor (
function genData$2 (line 11061) | function genData$2 (el, state) {
function genDirectives (line 11145) | function genDirectives (el, state) {
function genInlineTemplate (line 11170) | function genInlineTemplate (el, state) {
function genScopedSlots (line 11184) | function genScopedSlots (
function hash (line 11238) | function hash(str) {
function containsSlotChild (line 11247) | function containsSlotChild (el) {
function genScopedSlot (line 11257) | function genScopedSlot (
function genChildren (line 11282) | function genChildren (
function getNormalizationType (line 11315) | function getNormalizationType (
function needsNormalization (line 11338) | function needsNormalization (el) {
function genNode (line 11342) | function genNode (node, state) {
function genText (line 11352) | function genText (text) {
function genComment (line 11358) | function genComment (comment) {
function genSlot (line 11362) | function genSlot (el, state) {
function genComponent (line 11388) | function genComponent (
function genProps (line 11397) | function genProps (props) {
function transformSpecialNewlines (line 11418) | function transformSpecialNewlines (text) {
function detectErrors (line 11445) | function detectErrors (ast, warn) {
function checkNode (line 11451) | function checkNode (node, warn) {
function checkEvent (line 11478) | function checkEvent (exp, text, warn, range) {
function checkFor (line 11491) | function checkFor (node, text, warn, range) {
function checkIdentifier (line 11498) | function checkIdentifier (
function checkExpression (line 11514) | function checkExpression (exp, text, warn, range) {
function generateCodeFrame (line 11540) | function generateCodeFrame (
function repeat$1 (line 11577) | function repeat$1 (str, n) {
function createFunction (line 11594) | function createFunction (code, errors) {
function createCompileToFunctionFn (line 11603) | function createCompileToFunctionFn (compile) {
function createCompilerCreator (line 11705) | function createCompilerCreator (baseCompile) {
function getShouldDecode (line 11806) | function getShouldDecode (href) {
function getOuterHTML (line 11898) | function getOuterHTML (el) {
FILE: internal/webserver/handler-api-ext.go
method ApiInsertViaExtension (line 20) | func (h *Handler) ApiInsertViaExtension(w http.ResponseWriter, r *http.R...
method ApiDeleteViaExtension (line 121) | func (h *Handler) ApiDeleteViaExtension(w http.ResponseWriter, r *http.R...
FILE: internal/webserver/handler-api.go
function downloadBookmarkContent (line 21) | func downloadBookmarkContent(deps model.Dependencies, book *model.Bookma...
method ApiGetBookmarks (line 47) | func (h *Handler) ApiGetBookmarks(w http.ResponseWriter, r *http.Request...
method ApiGetTags (line 126) | func (h *Handler) ApiGetTags(w http.ResponseWriter, r *http.Request, ps ...
method ApiRenameTag (line 145) | func (h *Handler) ApiRenameTag(w http.ResponseWriter, r *http.Request, p...
type apiInsertBookmarkPayload (line 165) | type apiInsertBookmarkPayload struct
function newAPIInsertBookmarkPayload (line 178) | func newAPIInsertBookmarkPayload() *apiInsertBookmarkPayload {
method ApiInsertBookmark (line 187) | func (h *Handler) ApiInsertBookmark(w http.ResponseWriter, r *http.Reque...
method ApiDeleteBookmark (line 262) | func (h *Handler) ApiDeleteBookmark(w http.ResponseWriter, r *http.Reque...
method ApiUpdateBookmark (line 294) | func (h *Handler) ApiUpdateBookmark(w http.ResponseWriter, r *http.Reque...
method ApiUpdateBookmarkTags (line 374) | func (h *Handler) ApiUpdateBookmarkTags(w http.ResponseWriter, r *http.R...
FILE: internal/webserver/handler.go
type Handler (line 16) | type Handler struct
method PrepareSessionCache (line 29) | func (h *Handler) PrepareSessionCache() {
method validateSession (line 50) | func (h *Handler) validateSession(r *http.Request) error {
method tokenAccount (line 82) | func (h *Handler) tokenAccount(r *http.Request) (*model.AccountDTO, er...
method ssoAccount (line 111) | func (h *Handler) ssoAccount(r *http.Request) (*model.AccountDTO, erro...
method isTrustedIP (line 140) | func (h *Handler) isTrustedIP(ip net.IP) bool {
FILE: internal/webserver/server.go
type Config (line 12) | type Config struct
function GetLegacyHandler (line 22) | func GetLegacyHandler(cfg Config, dependencies model.Dependencies) *Hand...
FILE: internal/webserver/utils.go
function FileExists (line 9) | func FileExists(filePath string) bool {
function checkError (line 14) | func checkError(err error) {
FILE: internal/webserver/utils_ip.go
function parseCIDR (line 131) | func parseCIDR(network string, comment string) net.IPNet {
function isPrivateV4 (line 140) | func isPrivateV4(ip net.IP) bool {
function isPrivateV6 (line 150) | func isPrivateV6(ip net.IP) bool {
function IsPrivateIP (line 160) | func IsPrivateIP(ip net.IP) bool {
function IsIPValidAndPublic (line 168) | func IsIPValidAndPublic(ipAddr string) bool {
function GetUserRealIP (line 191) | func GetUserRealIP(r *http.Request) string {
FILE: internal/webserver/utils_ip_test.go
function TestParseCidr (line 15) | func TestParseCidr(t *testing.T) {
function TestParseCidrInvalidAddr (line 21) | func TestParseCidrInvalidAddr(t *testing.T) {
function TestIsPrivateIP (line 25) | func TestIsPrivateIP(t *testing.T) {
function TestIsIpValidAndPublic (line 56) | func TestIsIpValidAndPublic(t *testing.T) {
function BenchmarkIsPrivateIPv4 (line 72) | func BenchmarkIsPrivateIPv4(b *testing.B) {
function BenchmarkIsPrivateIPv6 (line 81) | func BenchmarkIsPrivateIPv6(b *testing.B) {
function testIsPublicHttpRequestAddressHelper (line 88) | func testIsPublicHttpRequestAddressHelper(
function testIsPublicHttpRequestAddressHelperWrapped (line 94) | func testIsPublicHttpRequestAddressHelperWrapped(
function TestGetUserRealIPWithSetRemoteAddr (line 128) | func TestGetUserRealIPWithSetRemoteAddr(t *testing.T) {
function TestGetUserRealIPWithInvalidRemoteAddr (line 138) | func TestGetUserRealIPWithInvalidRemoteAddr(t *testing.T) {
function TestGetUserRealIPWithEmptyHeader (line 149) | func TestGetUserRealIPWithEmptyHeader(t *testing.T) {
function TestGetUserRealIPWithInvalidHeaderValue (line 154) | func TestGetUserRealIPWithInvalidHeaderValue(t *testing.T) {
function TestGetUserRealIPWithXRealIpHeader (line 164) | func TestGetUserRealIPWithXRealIpHeader(t *testing.T) {
function TestGetUserRealIPWithPrivateXRealIpHeader (line 175) | func TestGetUserRealIPWithPrivateXRealIpHeader(t *testing.T) {
function TestGetUserRealIPWithXRealIpListHeader (line 186) | func TestGetUserRealIPWithXRealIpListHeader(t *testing.T) {
function TestGetUserRealIPWithXRealIpHeaderIgnoreComma (line 200) | func TestGetUserRealIPWithXRealIpHeaderIgnoreComma(t *testing.T) {
function TestGetUserRealIPWithDifferentHeaderOrder (line 215) | func TestGetUserRealIPWithDifferentHeaderOrder(t *testing.T) {
FILE: main.go
function init (line 18) | func init() {
function main (line 25) | func main() {
FILE: webapp/dist/assets/ArchiveView-DZOySksr.js
method setup (line 1) | setup(r){return(l,e)=>(i(),o(a,null,{header:t(()=>e[0]||(e[0]=[s("div",{...
FILE: webapp/dist/assets/FoldersView-B-TWh6ac.js
method setup (line 1) | setup(y){const r=i([{id:1,name:"Work",bookmarkCount:12},{id:2,name:"Pers...
FILE: webapp/dist/assets/SettingsView-BWJgD3kk.js
method setup (line 1) | setup($){const{t:r,locale:m}=u(),_=[{code:"en",name:"English"},{code:"es...
FILE: webapp/dist/assets/TagsView-CmDnarVi.js
class ae (line 1) | class ae extends U{async apiV1TagsGetRaw(e,s){const o={};e.withBookmarkC...
method apiV1TagsGetRaw (line 1) | async apiV1TagsGetRaw(e,s){const o={};e.withBookmarkCount!=null&&(o.wi...
method apiV1TagsGet (line 1) | async apiV1TagsGet(e={},s){return await(await this.apiV1TagsGetRaw(e,s...
method apiV1TagsIdDeleteRaw (line 1) | async apiV1TagsIdDeleteRaw(e,s){if(e.id==null)throw new k("id",'Requir...
method apiV1TagsIdDelete (line 1) | async apiV1TagsIdDelete(e,s){await this.apiV1TagsIdDeleteRaw(e,s)}
method apiV1TagsIdGetRaw (line 1) | async apiV1TagsIdGetRaw(e,s){if(e.id==null)throw new k("id",'Required ...
method apiV1TagsIdGet (line 1) | async apiV1TagsIdGet(e,s){return await(await this.apiV1TagsIdGetRaw(e,...
method apiV1TagsIdPutRaw (line 1) | async apiV1TagsIdPutRaw(e,s){if(e.id==null)throw new k("id",'Required ...
method apiV1TagsIdPut (line 1) | async apiV1TagsIdPut(e,s){return await(await this.apiV1TagsIdPutRaw(e,...
method apiV1TagsPostRaw (line 1) | async apiV1TagsPostRaw(e,s){if(e.tag==null)throw new k("tag",'Required...
method apiV1TagsPost (line 1) | async apiV1TagsPost(e,s){return await(await this.apiV1TagsPostRaw(e,s)...
method setup (line 1) | setup(h){const e=se(),s=F(),o=Q(),{tags:r,isLoading:u,error:w,fetchTags:...
FILE: webapp/dist/assets/index-C8c580-n.js
function n (line 2) | function n(s){const o={};return s.integrity&&(o.integrity=s.integrity),s...
function r (line 2) | function r(s){if(s.ep)return;s.ep=!0;const o=n(s);fetch(s.href,o)}
function Ys (line 6) | function Ys(e){const t=Object.create(null);for(const n of e.split(","))t...
function Qs (line 6) | function Qs(e){if(J(e)){const t={};for(let n=0;n<e.length;n++){const r=e...
function wc (line 6) | function wc(e){const t={};return e.replace(Tc,"").split(Ec).forEach(n=>{...
function $r (line 6) | function $r(e){let t="";if(Re(e))t=e;else if(J(e))for(let n=0;n<e.length...
function mi (line 6) | function mi(e){return!!e||e===""}
function Oc (line 6) | function Oc(e,t){if(e.length!==t.length)return!1;let n=!0;for(let r=0;n&...
function Ur (line 6) | function Ur(e,t){if(e===t)return!0;let n=Lo(e),r=Lo(t);if(n||r)return n&...
function gi (line 6) | function gi(e,t){return e.findIndex(n=>Ur(n,t))}
class bi (line 10) | class bi{constructor(t=!1){this.detached=t,this._active=!0,this.effects=...
method constructor (line 10) | constructor(t=!1){this.detached=t,this._active=!0,this.effects=[],this...
method active (line 10) | get active(){return this._active}
method pause (line 10) | pause(){if(this._active){this._isPaused=!0;let t,n;if(this.scopes)for(...
method resume (line 10) | resume(){if(this._active&&this._isPaused){this._isPaused=!1;let t,n;if...
method run (line 10) | run(t){if(this._active){const n=qe;try{return qe=this,t()}finally{qe=n}}}
method on (line 10) | on(){qe=this}
method off (line 10) | off(){qe=this.parent}
method stop (line 10) | stop(t){if(this._active){this._active=!1;let n,r;for(n=0,r=this.effect...
function Zs (line 10) | function Zs(e){return new bi(e)}
function yi (line 10) | function yi(){return qe}
function Ic (line 10) | function Ic(e,t=!1){qe&&qe.cleanups.push(e)}
class vi (line 10) | class vi{constructor(t){this.fn=t,this.deps=void 0,this.depsTail=void 0,...
method constructor (line 10) | constructor(t){this.fn=t,this.deps=void 0,this.depsTail=void 0,this.fl...
method pause (line 10) | pause(){this.flags|=64}
method resume (line 10) | resume(){this.flags&64&&(this.flags&=-65,os.has(this)&&(os.delete(this...
method notify (line 10) | notify(){this.flags&2&&!(this.flags&32)||this.flags&8||ki(this)}
method run (line 10) | run(){if(!(this.flags&1))return this.fn();this.flags|=2,Io(this),Ti(th...
method stop (line 10) | stop(){if(this.flags&1){for(let t=this.deps;t;t=t.nextDep)no(t);this.d...
method trigger (line 10) | trigger(){this.flags&64?os.add(this):this.scheduler?this.scheduler():t...
method runIfDirty (line 10) | runIfDirty(){Ts(this)&&this.run()}
method dirty (line 10) | get dirty(){return Ts(this)}
function ki (line 10) | function ki(e,t=!1){if(e.flags|=8,t){e.next=Vn,Vn=e;return}e.next=Un,Un=e}
function eo (line 10) | function eo(){Ei++}
function to (line 10) | function to(){if(--Ei>0)return;if(Vn){let t=Vn;for(Vn=void 0;t;){const n...
function Ti (line 10) | function Ti(e){for(let t=e.deps;t;t=t.nextDep)t.version=-1,t.prevActiveL...
function wi (line 10) | function wi(e){let t,n=e.depsTail,r=n;for(;r;){const s=r.prevDep;r.versi...
function Ts (line 10) | function Ts(e){for(let t=e.deps;t;t=t.nextDep)if(t.dep.version!==t.versi...
function Si (line 10) | function Si(e){if(e.flags&4&&!(e.flags&16)||(e.flags&=-17,e.globalVersio...
function no (line 10) | function no(e,t=!1){const{dep:n,prevSub:r,nextSub:s}=e;if(r&&(r.nextSub=...
function Ac (line 10) | function Ac(e){const{prevDep:t,nextDep:n}=e;t&&(t.nextDep=n,e.prevDep=vo...
function Gt (line 10) | function Gt(){Li.push(ft),ft=!1}
function jt (line 10) | function jt(){const e=Li.pop();ft=e===void 0?!0:e}
function Io (line 10) | function Io(e){const{cleanup:t}=e;if(e.cleanup=void 0,t){const n=be;be=v...
class Pc (line 10) | class Pc{constructor(t,n){this.sub=t,this.dep=n,this.version=n.version,t...
method constructor (line 10) | constructor(t,n){this.sub=t,this.dep=n,this.version=n.version,this.nex...
class ro (line 10) | class ro{constructor(t){this.computed=t,this.version=0,this.activeLink=v...
method constructor (line 10) | constructor(t){this.computed=t,this.version=0,this.activeLink=void 0,t...
method track (line 10) | track(t){if(!be||!ft||be===this.computed)return;let n=this.activeLink;...
method trigger (line 10) | trigger(t){this.version++,jn++,this.notify(t)}
method notify (line 10) | notify(t){eo();try{for(let n=this.subs;n;n=n.prevSub)n.sub.notify()&&n...
function Oi (line 10) | function Oi(e){if(e.dep.sc++,e.sub.flags&4){const t=e.dep.computed;if(t&...
function Ve (line 10) | function Ve(e,t,n){if(ft&&be){let r=Tr.get(e);r||Tr.set(e,r=new Map);let...
function Ot (line 10) | function Ot(e,t,n,r,s,o){const a=Tr.get(e);if(!a){jn++;return}const i=l=...
function Rc (line 10) | function Rc(e,t){const n=Tr.get(e);return n&&n.get(t)}
function dn (line 10) | function dn(e){const t=ie(e);return t===e?t:(Ve(t,"iterate",qn),at(e)?t:...
function Vr (line 10) | function Vr(e){return Ve(e=ie(e),"iterate",qn),e}
method [Symbol.iterator] (line 10) | [Symbol.iterator](){return as(this,Symbol.iterator,He)}
method concat (line 10) | concat(...e){return dn(this).concat(...e.map(t=>J(t)?dn(t):t))}
method entries (line 10) | entries(){return as(this,"entries",e=>(e[1]=He(e[1]),e))}
method every (line 10) | every(e,t){return kt(this,"every",e,t,void 0,arguments)}
method filter (line 10) | filter(e,t){return kt(this,"filter",e,t,n=>n.map(He),arguments)}
method find (line 10) | find(e,t){return kt(this,"find",e,t,He,arguments)}
method findIndex (line 10) | findIndex(e,t){return kt(this,"findIndex",e,t,void 0,arguments)}
method findLast (line 10) | findLast(e,t){return kt(this,"findLast",e,t,He,arguments)}
method findLastIndex (line 10) | findLastIndex(e,t){return kt(this,"findLastIndex",e,t,void 0,arguments)}
method forEach (line 10) | forEach(e,t){return kt(this,"forEach",e,t,void 0,arguments)}
method includes (line 10) | includes(...e){return is(this,"includes",e)}
method indexOf (line 10) | indexOf(...e){return is(this,"indexOf",e)}
method join (line 10) | join(e){return dn(this).join(e)}
method lastIndexOf (line 10) | lastIndexOf(...e){return is(this,"lastIndexOf",e)}
method map (line 10) | map(e,t){return kt(this,"map",e,t,void 0,arguments)}
method pop (line 10) | pop(){return Nn(this,"pop")}
method push (line 10) | push(...e){return Nn(this,"push",e)}
method reduce (line 10) | reduce(e,...t){return Ao(this,"reduce",e,t)}
method reduceRight (line 10) | reduceRight(e,...t){return Ao(this,"reduceRight",e,t)}
method shift (line 10) | shift(){return Nn(this,"shift")}
method some (line 10) | some(e,t){return kt(this,"some",e,t,void 0,arguments)}
method splice (line 10) | splice(...e){return Nn(this,"splice",e)}
method toReversed (line 10) | toReversed(){return dn(this).toReversed()}
method toSorted (line 10) | toSorted(e){return dn(this).toSorted(e)}
method toSpliced (line 10) | toSpliced(...e){return dn(this).toSpliced(...e)}
method unshift (line 10) | unshift(...e){return Nn(this,"unshift",e)}
method values (line 10) | values(){return as(this,"values",He)}
function as (line 10) | function as(e,t,n){const r=Vr(e),s=r[t]();return r!==e&&!at(e)&&(s._next...
function kt (line 10) | function kt(e,t,n,r,s,o){const a=Vr(e),i=a!==e&&!at(e),l=a[t];if(l!==Nc[...
function Ao (line 10) | function Ao(e,t,n,r){const s=Vr(e);let o=n;return s!==e&&(at(e)?n.length...
function is (line 10) | function is(e,t,n){const r=ie(e);Ve(r,"iterate",qn);const s=r[t](...n);r...
function Nn (line 10) | function Nn(e,t,n=[]){Gt(),eo();const r=ie(e)[t].apply(e,n);return to(),...
function Mc (line 10) | function Mc(e){dt(e)||(e=String(e));const t=ie(this);return Ve(t,"has",e...
class Ai (line 10) | class Ai{constructor(t=!1,n=!1){this._isReadonly=t,this._isShallow=n}get...
method constructor (line 10) | constructor(t=!1,n=!1){this._isReadonly=t,this._isShallow=n}
method get (line 10) | get(t,n,r){if(n==="__v_skip")return t.__v_skip;const s=this._isReadonl...
class Pi (line 10) | class Pi extends Ai{constructor(t=!1){super(!1,t)}set(t,n,r,s){let o=t[n...
method constructor (line 10) | constructor(t=!1){super(!1,t)}
method set (line 10) | set(t,n,r,s){let o=t[n];if(!this._isShallow){const l=cn(o);if(!at(r)&&...
method deleteProperty (line 10) | deleteProperty(t,n){const r=he(t,n);t[n];const s=Reflect.deletePropert...
method has (line 10) | has(t,n){const r=Reflect.has(t,n);return(!dt(n)||!Ii.has(n))&&Ve(t,"ha...
method ownKeys (line 10) | ownKeys(t){return Ve(t,"iterate",J(t)?"length":an),Reflect.ownKeys(t)}
class Dc (line 10) | class Dc extends Ai{constructor(t=!1){super(!0,t)}set(t,n){return!0}dele...
method constructor (line 10) | constructor(t=!1){super(!0,t)}
method set (line 10) | set(t,n){return!0}
method deleteProperty (line 10) | deleteProperty(t,n){return!0}
function Vc (line 10) | function Vc(e,t,n){return function(...r){const s=this.__v_raw,o=ie(s),a=...
function dr (line 10) | function dr(e){return function(...t){return e==="delete"?!1:e==="clear"?...
function Hc (line 10) | function Hc(e,t){const n={get(s){const o=this.__v_raw,a=ie(o),i=ie(s);e|...
function so (line 10) | function so(e,t){const n=Hc(e,t);return(r,s,o)=>s==="__v_isReactive"?!e:...
function jc (line 10) | function jc(e){switch(e){case"Object":case"Array":return 1;case"Map":cas...
function qc (line 10) | function qc(e){return e.__v_skip||!Object.isExtensible(e)?0:jc(bc(e))}
function ar (line 10) | function ar(e){return cn(e)?e:oo(e,!1,Fc,Wc,Ri)}
function xi (line 10) | function xi(e){return oo(e,!1,Uc,Bc,Ci)}
function Mi (line 10) | function Mi(e){return oo(e,!0,$c,Kc,Ni)}
function oo (line 10) | function oo(e,t,n,r,s){if(!ve(e)||e.__v_raw&&!(t&&e.__v_isReactive))retu...
function Ut (line 10) | function Ut(e){return cn(e)?Ut(e.__v_raw):!!(e&&e.__v_isReactive)}
function cn (line 10) | function cn(e){return!!(e&&e.__v_isReadonly)}
function at (line 10) | function at(e){return!!(e&&e.__v_isShallow)}
function ao (line 10) | function ao(e){return e?!!e.__v_raw:!1}
function ie (line 10) | function ie(e){const t=e&&e.__v_raw;return t?ie(t):e}
function io (line 10) | function io(e){return!he(e,"__v_skip")&&Object.isExtensible(e)&&hi(e,"__...
function Oe (line 10) | function Oe(e){return e?e.__v_isRef===!0:!1}
function ge (line 10) | function ge(e){return Di(e,!1)}
function lo (line 10) | function lo(e){return Di(e,!0)}
function Di (line 10) | function Di(e,t){return Oe(e)?e:new Yc(e,t)}
class Yc (line 10) | class Yc{constructor(t,n){this.dep=new ro,this.__v_isRef=!0,this.__v_isS...
method constructor (line 10) | constructor(t,n){this.dep=new ro,this.__v_isRef=!0,this.__v_isShallow=...
method value (line 10) | get value(){return this.dep.track(),this._value}
method value (line 10) | set value(t){const n=this._rawValue,r=this.__v_isShallow||at(t)||cn(t)...
function le (line 10) | function le(e){return Oe(e)?e.value:e}
function Fi (line 10) | function Fi(e){return Ut(e)?e:new Proxy(e,Jc)}
function Xc (line 10) | function Xc(e){const t=J(e)?new Array(e.length):{};for(const n in e)t[n]...
class zc (line 10) | class zc{constructor(t,n,r){this._object=t,this._key=n,this._defaultValu...
method constructor (line 10) | constructor(t,n,r){this._object=t,this._key=n,this._defaultValue=r,thi...
method value (line 10) | get value(){const t=this._object[this._key];return this._value=t===voi...
method value (line 10) | set value(t){this._object[this._key]=t}
method dep (line 10) | get dep(){return Rc(ie(this._object),this._key)}
function Qc (line 10) | function Qc(e,t,n){const r=e[t];return Oe(r)?r:new zc(e,t,n)}
class Zc (line 10) | class Zc{constructor(t,n,r){this.fn=t,this.setter=n,this._value=void 0,t...
method constructor (line 10) | constructor(t,n,r){this.fn=t,this.setter=n,this._value=void 0,this.dep...
method notify (line 10) | notify(){if(this.flags|=16,!(this.flags&8)&&be!==this)return ki(this,!...
method value (line 10) | get value(){const t=this.dep.track();return Si(this),t&&(t.version=thi...
method value (line 10) | set value(t){this.setter&&this.setter(t)}
function eu (line 10) | function eu(e,t,n=!1){let r,s;return te(e)?r=e:(r=e.get,s=e.set),new Zc(...
function tu (line 10) | function tu(e,t=!1,n=sn){if(n){let r=wr.get(n);r||wr.set(n,r=[]),r.push(...
function nu (line 10) | function nu(e,t,n=_e){const{immediate:r,deep:s,once:o,scheduler:a,augmen...
function It (line 10) | function It(e,t=1/0,n){if(t<=0||!ve(e)||e.__v_skip||(n=n||new Set,n.has(...
function ir (line 14) | function ir(e,t,n,r){try{return r?e(...r):e()}catch(s){Hr(s,t,n)}}
function Et (line 14) | function Et(e,t,n,r){if(te(e)){const s=ir(e,t,n,r);return s&&ci(s)&&s.ca...
function Hr (line 14) | function Hr(e,t,n,r=!0){const s=t?t.vnode:null,{errorHandler:o,throwUnha...
function ru (line 14) | function ru(e,t,n,r=!0,s=!1){if(s)throw e;console.error(e)}
function co (line 14) | function co(e){const t=Sr||$i;return e?t.then(this?e.bind(this):e):t}
function su (line 14) | function su(e){let t=_t+1,n=Ye.length;for(;t<n;){const r=t+n>>>1,s=Ye[r]...
function uo (line 14) | function uo(e){if(!(e.flags&1)){const t=Yn(e),n=Ye[Ye.length-1];!n||!(e....
function Ui (line 14) | function Ui(){Sr||(Sr=$i.then(Hi))}
function ou (line 14) | function ou(e){J(e)?vn.push(...e):Mt&&e.id===-1?Mt.splice(mn+1,0,e):e.fl...
function Po (line 14) | function Po(e,t,n=_t+1){for(;n<Ye.length;n++){const r=Ye[n];if(r&&r.flag...
function Vi (line 14) | function Vi(e){if(vn.length){const t=[...new Set(vn)].sort((n,r)=>Yn(n)-...
function Hi (line 14) | function Hi(e){try{for(_t=0;_t<Ye.length;_t++){const t=Ye[_t];t&&!(t.fla...
function Lr (line 14) | function Lr(e){const t=Me;return Me=e,Wi=e&&e.type.__scopeId||null,t}
function En (line 14) | function En(e,t=Me,n){if(!t||e._n)return e;const r=(...s)=>{r._d&&Vo(-1)...
function ls (line 14) | function ls(e,t){if(Me===null)return e;const n=Kr(Me),r=e.dirs||(e.dirs=...
function Zt (line 14) | function Zt(e,t,n,r){const s=e.dirs,o=t&&t.dirs;for(let a=0;a<s.length;a...
function fo (line 14) | function fo(e,t){e.shapeFlag&6&&e.component?(e.transition=t,fo(e.compone...
function it (line 14) | function it(e,t){return te(e)?We({name:e.name},t,{setup:e}):e}
function Bi (line 14) | function Bi(e){e.ids=[e.ids[0]+e.ids[2]+++"-",0,0]}
function Or (line 14) | function Or(e,t,n,r,s=!1){if(J(e)){e.forEach((S,T)=>Or(S,t&&(J(t)?t[T]:t...
function lu (line 14) | function lu(e,t){Gi(e,"a",t)}
function cu (line 14) | function cu(e,t){Gi(e,"da",t)}
function Gi (line 14) | function Gi(e,t,n=$e){const r=e.__wdc||(e.__wdc=()=>{let s=n;for(;s;){if...
function uu (line 14) | function uu(e,t,n,r){const s=Wr(t,e,r,!0);Rn(()=>{Xs(r[t],s)},n)}
function Wr (line 14) | function Wr(e,t,n=$e,r=!1){if(n){const s=n[e]||(n[e]=[]),o=t.__weh||(t._...
function _u (line 14) | function _u(e,t=$e){Wr("ec",e,t)}
function Jn (line 14) | function Jn(e,t,n,r){let s;const o=n,a=J(e);if(a||Re(e)){const i=a&&Ut(e...
function Ro (line 14) | function Ro(e,t,n={},r,s){if(Me.ce||Me.parent&&kn(Me.parent)&&Me.parent....
function qi (line 14) | function qi(e){return e.some(t=>zn(t)?!(t.type===Bt||t.type===Ce&&!qi(t....
method get (line 14) | get({_:e},t){if(t==="__v_skip")return!0;const{ctx:n,setupState:r,data:s,...
method set (line 14) | set({_:e},t,n){const{data:r,setupState:s,ctx:o}=e;return cs(s,t)?(s[t]=n...
method has (line 14) | has({_:{data:e,setupState:t,accessCache:n,ctx:r,appContext:s,propsOption...
method defineProperty (line 14) | defineProperty(e,t,n){return n.get!=null?e._.accessCache[t]=0:he(n,"valu...
function Co (line 14) | function Co(e){return J(e)?e.reduce((t,n)=>(t[n]=null,t),{}):e}
function vu (line 14) | function vu(e){const t=Ji(e),n=e.proxy,r=e.ctx;Is=!1,t.beforeCreate&&No(...
function Eu (line 14) | function Eu(e,t,n=vt){J(e)&&(e=As(e));for(const r in e){const s=e[r];let...
function No (line 14) | function No(e,t,n){Et(J(e)?e.map(r=>r.bind(t.proxy)):e.bind(t.proxy),t,n)}
function Yi (line 14) | function Yi(e,t,n,r){let s=r.includes(".")?cl(n,r):()=>n[r];if(Re(e)){co...
function Ji (line 14) | function Ji(e){const t=e.type,{mixins:n,extends:r}=t,{mixins:s,optionsCa...
function Ir (line 14) | function Ir(e,t,n,r=!1){const{mixins:s,extends:o}=t;o&&Ir(e,o,n,!0),s&&s...
function xo (line 14) | function xo(e,t){return t?e?function(){return We(te(e)?e.call(this,this)...
function Tu (line 14) | function Tu(e,t){return Fn(As(e),As(t))}
function As (line 14) | function As(e){if(J(e)){const t={};for(let n=0;n<e.length;n++)t[e[n]]=e[...
function Ge (line 14) | function Ge(e,t){return e?[...new Set([].concat(e,t))]:t}
function Fn (line 14) | function Fn(e,t){return e?We(Object.create(null),e,t):t}
function Mo (line 14) | function Mo(e,t){return e?J(e)&&J(t)?[...new Set([...e,...t])]:We(Object...
function wu (line 14) | function wu(e,t){if(!e)return t;if(!t)return e;const n=We(Object.create(...
function Xi (line 14) | function Xi(){return{app:null,config:{isNativeTag:pc,performance:!1,glob...
function Lu (line 14) | function Lu(e,t){return function(r,s=null){te(r)||(r=We({},r)),s!=null&&...
function br (line 14) | function br(e,t){if($e){let n=$e.provides;const r=$e.parent&&$e.parent.p...
function rt (line 14) | function rt(e,t,n=!1){const r=$e||Me;if(r||ln){const s=ln?ln._context.pr...
function Ou (line 14) | function Ou(){return!!($e||Me||ln)}
function Iu (line 14) | function Iu(e,t,n,r=!1){const s={},o=Qi();e.propsDefaults=Object.create(...
function Au (line 14) | function Au(e,t,n,r){const{props:s,attrs:o,vnode:{patchFlag:a}}=e,i=ie(s...
function el (line 14) | function el(e,t,n,r){const[s,o]=e.propsOptions;let a=!1,i;if(t)for(let l...
function Ps (line 14) | function Ps(e,t,n,r,s,o){const a=e[n];if(a!=null){const i=he(a,"default"...
function tl (line 14) | function tl(e,t,n=!1){const r=n?Pu:t.propsCache,s=r.get(e);if(s)return s...
function Do (line 14) | function Do(e){return e[0]!=="$"&&!$n(e)}
function xu (line 14) | function xu(e){return Mu(e)}
function Mu (line 14) | function Mu(e,t){const n=Fr();n.__VUE__=!0;const{insert:r,remove:s,patch...
function us (line 14) | function us({type:e,props:t},n){return n==="svg"&&e==="foreignObject"||n...
function en (line 14) | function en({effect:e,job:t},n){n?(e.flags|=32,t.flags|=4):(e.flags&=-33...
function Du (line 14) | function Du(e,t){return(!e||e&&!e.pendingBranch)&&t&&!t.persisted}
function al (line 14) | function al(e,t,n=!1){const r=e.children,s=t.children;if(J(r)&&J(s))for(...
function Fu (line 14) | function Fu(e){const t=e.slice(),n=[0];let r,s,o,a,i;const l=e.length;fo...
function il (line 14) | function il(e){const t=e.subTree.component;if(t)return t.asyncDep&&!t.as...
function Fo (line 14) | function Fo(e){if(e)for(let t=0;t<e.length;t++)e[t].flags|=8}
function Vt (line 14) | function Vt(e,t,n){return ll(e,t,n)}
function ll (line 14) | function ll(e,t,n=_e){const{immediate:r,deep:s,flush:o,once:a}=n,i=We({}...
function Vu (line 14) | function Vu(e,t,n){const r=this.proxy,s=Re(e)?e.includes(".")?cl(r,e):()...
function cl (line 14) | function cl(e,t){const n=t.split(".");return()=>{let r=e;for(let s=0;s<n...
function Wu (line 14) | function Wu(e,t,...n){if(e.isUnmounted)return;const r=e.vnode.props||_e;...
function ul (line 14) | function ul(e,t,n=!1){const r=t.emitsCache,s=r.get(e);if(s!==void 0)retu...
function Br (line 14) | function Br(e,t){return!e||!xr(t)?!1:(t=t.slice(2).replace(/Once$/,""),h...
function $o (line 14) | function $o(e){const{type:t,vnode:n,proxy:r,withProxy:s,propsOptions:[o]...
function Gu (line 14) | function Gu(e,t,n){const{props:r,children:s,component:o}=e,{props:a,chil...
function Uo (line 14) | function Uo(e,t,n){const r=Object.keys(t);if(r.length!==Object.keys(e).l...
function ju (line 14) | function ju({vnode:e,parent:t},n){for(;t;){const r=t.subTree;if(r.suspen...
function qu (line 14) | function qu(e,t){t&&t.pendingBranch?J(e)?t.effects.push(...e):t.effects....
function fe (line 14) | function fe(e=!1){Wn.push(nt=e?null:[])}
function Yu (line 14) | function Yu(){Wn.pop(),nt=Wn[Wn.length-1]||null}
function Vo (line 14) | function Vo(e,t=!1){Xn+=e,e<0&&nt&&t&&(nt.hasOnce=!0)}
function dl (line 14) | function dl(e){return e.dynamicChildren=Xn>0?nt||bn:null,Yu(),Xn>0&&nt&&...
function Ee (line 14) | function Ee(e,t,n,r,s,o){return dl(H(e,t,n,r,s,o,!0))}
function wn (line 14) | function wn(e,t,n,r,s){return dl(Pe(e,t,n,r,s,!0))}
function zn (line 14) | function zn(e){return e?e.__v_isVNode===!0:!1}
function xn (line 14) | function xn(e,t){return e.type===t.type&&e.key===t.key}
function H (line 14) | function H(e,t=null,n=null,r=0,s=null,o=e===Ce?0:1,a=!1,i=!1){const l={_...
function Ju (line 14) | function Ju(e,t=null,n=null,r=0,s=null,o=!1){if((!e||e===bu)&&(e=Bt),zn(...
function Xu (line 14) | function Xu(e){return e?ao(e)||Zi(e)?We({},e):e:null}
function Sn (line 14) | function Sn(e,t,n=!1,r=!1){const{props:s,ref:o,patchFlag:a,children:i,tr...
function ml (line 14) | function ml(e=" ",t=0){return Pe(lr,null,e,t)}
function Ht (line 14) | function Ht(e="",t=!1){return t?(fe(),wn(Bt,null,e)):Pe(Bt,null,e)}
function bt (line 14) | function bt(e){return e==null||typeof e=="boolean"?Pe(Bt):J(e)?Pe(Ce,nul...
function Dt (line 14) | function Dt(e){return e.el===null&&e.patchFlag!==-1||e.memo?e:Sn(e)}
function mo (line 14) | function mo(e,t){let n=0;const{shapeFlag:r}=e;if(t==null)t=null;else if(...
function zu (line 14) | function zu(...e){const t={};for(let n=0;n<e.length;n++){const r=e[n];fo...
function pt (line 14) | function pt(e,t,n,r=null){Et(e,t,7,[n,r])}
function ef (line 14) | function ef(e,t,n){const r=e.type,s=(t?t.appContext:e.appContext)||Qu,o=...
function gl (line 14) | function gl(e){return e.vnode.shapeFlag&4}
function tf (line 14) | function tf(e,t=!1,n=!1){t&&Rs(t);const{props:r,children:s}=e.vnode,o=gl...
function nf (line 14) | function nf(e,t){const n=e.type;e.accessCache=Object.create(null),e.prox...
function Wo (line 14) | function Wo(e,t,n){te(t)?e.type.__ssrInlineRender?e.ssrRender=t:e.render...
function pl (line 14) | function pl(e,t,n){const r=e.type;e.render||(e.render=r.render||vt);{con...
method get (line 14) | get(e,t){return Ve(e,"get",""),e[t]}
function sf (line 14) | function sf(e){const t=n=>{e.exposed=n||{}};return{attrs:new Proxy(e.att...
function Kr (line 14) | function Kr(e){return e.exposed?e.exposeProxy||(e.exposeProxy=new Proxy(...
function of (line 14) | function of(e){return te(e)&&"__vccOpts"in e}
function Gr (line 14) | function Gr(e,t,n){const r=arguments.length;return r===2?ve(t)&&!J(t)?zn...
method setScopeId (line 18) | setScopeId(e,t){e.setAttribute(t,"")}
method insertStaticContent (line 18) | insertStaticContent(e,t,n,r,s,o){const a=n?n.previousSibling:t.lastChild...
function df (line 18) | function df(e,t,n){const r=e[ff];r&&(t=(t?[t,...r]:[...r]).join(" ")),t=...
function pf (line 18) | function pf(e,t,n){const r=e.style,s=Re(n);let o=!1;if(n&&!s){if(t)if(Re...
function vr (line 18) | function vr(e,t,n){if(J(n))n.forEach(r=>vr(e,t,r));else if(n==null&&(n="...
function _f (line 18) | function _f(e,t){const n=ds[t];if(n)return n;let r=Wt(t);if(r!=="filter"...
function Jo (line 18) | function Jo(e,t,n,r,s,o=Lc(t)){r&&t.startsWith("xlink:")?n==null?e.remov...
function Xo (line 18) | function Xo(e,t,n,r,s){if(t==="innerHTML"||t==="textContent"){n!=null&&(...
function on (line 18) | function on(e,t,n,r){e.addEventListener(t,n,r)}
function bf (line 18) | function bf(e,t,n,r){e.removeEventListener(t,n,r)}
function yf (line 18) | function yf(e,t,n,r,s=null){const o=e[zo]||(e[zo]={}),a=o[t];if(r&&a)a.v...
function vf (line 18) | function vf(e){let t;if(Qo.test(e)){t={};let r;for(;r=e.match(Qo);)e=e.s...
function Tf (line 18) | function Tf(e,t){const n=r=>{if(!r._vts)r._vts=Date.now();else if(r._vts...
function wf (line 18) | function wf(e,t){if(J(t)){const n=e.stopImmediatePropagation;return e.st...
function Lf (line 18) | function Lf(e,t,n,r){if(r)return!!(t==="innerHTML"||t==="textContent"||t...
function Of (line 18) | function Of(e){e.target.composing=!0}
function ea (line 18) | function ea(e){const t=e.target;t.composing&&(t.composing=!1,t.dispatchE...
method created (line 18) | created(e,{modifiers:{lazy:t,trim:n,number:r}},s){e[Tn]=Pr(s);const o=r|...
method mounted (line 18) | mounted(e,{value:t}){e.value=t??""}
method beforeUpdate (line 18) | beforeUpdate(e,{value:t,oldValue:n,modifiers:{lazy:r,trim:s,number:o}},a...
method created (line 18) | created(e,t,n){e[Tn]=Pr(n),on(e,"change",()=>{const r=e._modelValue,s=Af...
method beforeUpdate (line 18) | beforeUpdate(e,t,n){e[Tn]=Pr(n),na(e,t,n)}
function na (line 18) | function na(e,{value:t,oldValue:n},r){e._modelValue=t;let s;if(J(t))s=gi...
function Af (line 18) | function Af(e){return"_value"in e?e._value:e.value}
function bl (line 18) | function bl(e,t){const n=t?"_trueValue":"_falseValue";return n in e?e[n]:t}
function Nf (line 18) | function Nf(){return ra||(ra=xu(Cf))}
function Mf (line 18) | function Mf(e){if(e instanceof SVGElement)return"svg";if(typeof MathMLEl...
function Df (line 18) | function Df(e){return Re(e)?document.querySelector(e):e}
function Ns (line 22) | function Ns(e){return e&&typeof e=="object"&&Object.prototype.toString.c...
function Ff (line 22) | function Ff(){const e=Zs(!0),t=e.run(()=>ge({}));let n=[],r=[];const s=i...
function sa (line 22) | function sa(e,t,n,r=El){e.push(t);const s=()=>{const o=e.indexOf(t);o>-1...
function hn (line 22) | function hn(e,...t){e.slice().forEach(n=>{n(...t)})}
function xs (line 22) | function xs(e,t){e instanceof Map&&t instanceof Map?t.forEach((n,r)=>e.s...
function Vf (line 22) | function Vf(e){return!Ns(e)||!e.hasOwnProperty(Uf)}
function Hf (line 22) | function Hf(e){return!!(Oe(e)&&e.effect)}
function Wf (line 22) | function Wf(e,t,n,r){const{state:s,actions:o,getters:a}=t,i=n.state.valu...
function kl (line 22) | function kl(e,t,n={},r,s,o){let a;const i=xt({actions:{}},n),l={deep:!0}...
function Bf (line 22) | function Bf(e,t,n){let r;const s=typeof t=="function";r=s?n:t;function o...
function Tl (line 26) | function Tl(e){return typeof e=="object"||"displayName"in e||"props"in e...
function Kf (line 26) | function Kf(e){return e.__esModule||e[Symbol.toStringTag]==="Module"||e....
function gs (line 26) | function gs(e,t){const n={};for(const r in t){const s=t[r];n[r]=ht(s)?s....
function go (line 26) | function go(e){return encodeURI(""+e).replace(Qf,"|").replace(Jf,"[").re...
function ed (line 26) | function ed(e){return go(e).replace(Ol,"{").replace(Il,"}").replace(Ll,"...
function Ms (line 26) | function Ms(e){return go(e).replace(Sl,"%2B").replace(Zf,"+").replace(wl...
function td (line 26) | function td(e){return Ms(e).replace(qf,"%3D")}
function nd (line 26) | function nd(e){return go(e).replace(wl,"%23").replace(Yf,"%3F")}
function rd (line 26) | function rd(e){return e==null?"":nd(e).replace(jf,"%2F")}
function er (line 26) | function er(e){try{return decodeURIComponent(""+e)}catch{}return""+e}
function ps (line 26) | function ps(e,t,n="/"){let r,s={},o="",a="";const i=t.indexOf("#");let l...
function ad (line 26) | function ad(e,t){const n=t.query?e(t.query):"";return t.path+(n&&"?")+n+...
function aa (line 26) | function aa(e,t){return!t||!e.toLowerCase().startsWith(t.toLowerCase())?...
function id (line 26) | function id(e,t,n){const r=t.matched.length-1,s=n.matched.length-1;retur...
function Ln (line 26) | function Ln(e,t){return(e.aliasOf||e)===(t.aliasOf||t)}
function Al (line 26) | function Al(e,t){if(Object.keys(e).length!==Object.keys(t).length)return...
function ld (line 26) | function ld(e,t){return ht(e)?ia(e,t):ht(t)?ia(t,e):e===t}
function ia (line 26) | function ia(e,t){return ht(t)?e.length===t.length&&e.every((n,r)=>n===t[...
function cd (line 26) | function cd(e,t){if(e.startsWith("/"))return e;if(!e)return t;const n=t....
function ud (line 26) | function ud(e){if(!e)if(gn){const t=document.querySelector("base");e=t&&...
function dd (line 26) | function dd(e,t){return e.replace(fd,"#")+t}
function hd (line 26) | function hd(e,t){const n=document.documentElement.getBoundingClientRect(...
function md (line 26) | function md(e){let t;if("el"in e){const n=e.el,r=typeof n=="string"&&n.s...
function la (line 26) | function la(e,t){return(history.state?history.state.position-t:-1)+e}
function gd (line 26) | function gd(e,t){Ds.set(e,t)}
function pd (line 26) | function pd(e){const t=Ds.get(e);return Ds.delete(e),t}
function Pl (line 26) | function Pl(e,t){const{pathname:n,search:r,hash:s}=t,o=e.indexOf("#");if...
function bd (line 26) | function bd(e,t,n,r){let s=[],o=[],a=null;const i=({state:m})=>{const _=...
function ca (line 26) | function ca(e,t,n,r=!1,s=!1){return{back:e,current:t,forward:n,replaced:...
function yd (line 26) | function yd(e){const{history:t,location:n}=window,r={value:Pl(e,n)},s={v...
function vd (line 26) | function vd(e){e=ud(e);const t=yd(e),n=bd(e,t.state,t.location,t.replace...
function Ed (line 26) | function Ed(e){return typeof e=="string"||e&&typeof e=="object"}
function Rl (line 26) | function Rl(e){return typeof e=="string"||typeof e=="symbol"}
function On (line 26) | function On(e,t){return de(new Error,{type:e,[Cl]:!0},t)}
function Tt (line 26) | function Tt(e,t){return e instanceof Error&&Cl in e&&(t==null||!!(e.type...
function wd (line 26) | function wd(e,t){const n=de({},kd,t),r=[];let s=n.start?"^":"";const o=[...
function Sd (line 26) | function Sd(e,t){let n=0;for(;n<e.length&&n<t.length;){const r=t[n]-e[n]...
function Nl (line 26) | function Nl(e,t){let n=0;const r=e.score,s=t.score;for(;n<r.length&&n<s....
function da (line 26) | function da(e){const t=e[e.length-1];return e.length>0&&t[t.length-1]<0}
function Id (line 26) | function Id(e){if(!e)return[[]];if(e==="/")return[[Ld]];if(!e.startsWith...
function Ad (line 26) | function Ad(e,t,n){const r=wd(Id(e.path),n),s=de(r,{record:e,parent:t,ch...
function Pd (line 26) | function Pd(e,t){const n=[],r=new Map;t=pa({strict:!1,end:!0,sensitive:!...
function ha (line 26) | function ha(e,t){const n={};for(const r of t)r in e&&(n[r]=e[r]);return n}
function ma (line 26) | function ma(e){const t={path:e.path,redirect:e.redirect,name:e.name,meta...
function Rd (line 26) | function Rd(e){const t={},n=e.props||!1;if("component"in e)t.default=n;e...
function ga (line 26) | function ga(e){for(;e;){if(e.record.aliasOf)return!0;e=e.parent}return!1}
function Cd (line 26) | function Cd(e){return e.reduce((t,n)=>de(t,n.meta),{})}
function pa (line 26) | function pa(e,t){const n={};for(const r in e)n[r]=r in t?t[r]:e[r];retur...
function Nd (line 26) | function Nd(e,t){let n=0,r=t.length;for(;n!==r;){const o=n+r>>1;Nl(e,t[o...
function xd (line 26) | function xd(e){let t=e;for(;t=t.parent;)if(xl(t)&&Nl(e,t)===0)return t}
function xl (line 26) | function xl({record:e}){return!!(e.name||e.components&&Object.keys(e.com...
function Md (line 26) | function Md(e){const t={};if(e===""||e==="?")return t;const r=(e[0]==="?...
function _a (line 26) | function _a(e){let t="";for(let n in e){const r=e[n];if(n=td(n),r==null)...
function Dd (line 26) | function Dd(e){const t={};for(const n in e){const r=e[n];r!==void 0&&(t[...
function Mn (line 26) | function Mn(){let e=[];function t(r){return e.push(r),()=>{const s=e.ind...
function Ft (line 26) | function Ft(e,t,n,r,s,o=a=>a()){const a=r&&(r.enterCallbacks[s]=r.enterC...
function _s (line 26) | function _s(e,t,n,r,s=o=>o()){const o=[];for(const a of e)for(const i in...
function ya (line 26) | function ya(e){const t=rt(Jr),n=rt(po),r=we(()=>{const l=le(e.to);return...
function $d (line 26) | function $d(e){return e.length===1?e[0]:e}
method setup (line 26) | setup(e,{slots:t}){const n=ar(ya(e)),{options:r}=rt(Jr),s=we(()=>({[Ea(e...
function Vd (line 26) | function Vd(e){if(!(e.metaKey||e.altKey||e.ctrlKey||e.shiftKey)&&!e.defa...
function Hd (line 26) | function Hd(e,t){for(const n in t){const r=t[n],s=e[n];if(typeof r=="str...
function va (line 26) | function va(e){return e?e.aliasOf?e.aliasOf.path:e.path:""}
method setup (line 26) | setup(e,{attrs:t,slots:n}){const r=rt(Fs),s=we(()=>e.route||r.value),o=r...
function ka (line 26) | function ka(e,t){if(!e)return null;const n=e(t);return n.length===1?n[0]:n}
function Bd (line 26) | function Bd(e){const t=Pd(e.routes,e),n=e.parseQuery||Md,r=e.stringifyQu...
function Kd (line 26) | function Kd(e,t){const n=[],r=[],s=[],o=Math.max(t.matched.length,e.matc...
function Xr (line 26) | function Xr(){return rt(Jr)}
function Gd (line 26) | function Gd(e){return rt(po)}
class Dl (line 26) | class Dl{constructor(t={}){this.configuration=t}set config(t){this.confi...
method constructor (line 26) | constructor(t={}){this.configuration=t}
method config (line 26) | set config(t){this.configuration=t}
method basePath (line 26) | get basePath(){return this.configuration.basePath!=null?this.configura...
method fetchApi (line 26) | get fetchApi(){return this.configuration.fetchApi}
method middleware (line 26) | get middleware(){return this.configuration.middleware||[]}
method queryParamsStringify (line 26) | get queryParamsStringify(){return this.configuration.queryParamsString...
method username (line 26) | get username(){return this.configuration.username}
method password (line 26) | get password(){return this.configuration.password}
method apiKey (line 26) | get apiKey(){const t=this.configuration.apiKey;if(t)return typeof t=="...
method accessToken (line 26) | get accessToken(){const t=this.configuration.accessToken;if(t)return t...
method headers (line 26) | get headers(){return this.configuration.headers}
method credentials (line 26) | get credentials(){return this.configuration.credentials}
method constructor (line 26) | constructor(t=qd){Qt(this,"middleware");Qt(this,"fetchApi",async(t,n)=>{...
method withMiddleware (line 26) | withMiddleware(...t){const n=this.clone();return n.middleware=n.middlewa...
method withPreMiddleware (line 26) | withPreMiddleware(...t){const n=t.map(r=>({pre:r}));return this.withMidd...
method withPostMiddleware (line 26) | withPostMiddleware(...t){const n=t.map(r=>({post:r}));return this.withMi...
method isJsonMime (line 26) | isJsonMime(t){return t?Nr.jsonRegex.test(t):!1}
method request (line 26) | async request(t,n){const{url:r,init:s}=await this.createFetchParams(t,n)...
method createFetchParams (line 26) | async createFetchParams(t,n){let r=this.configuration.basePath+t.path;t....
method clone (line 26) | clone(){const t=this.constructor,n=new t(this.configuration);return n.mi...
function Yd (line 26) | function Yd(e){return typeof Blob<"u"&&e instanceof Blob}
function Jd (line 26) | function Jd(e){return typeof FormData<"u"&&e instanceof FormData}
class Xd (line 26) | class Xd extends Error{constructor(n,r){super(r);Qt(this,"name","Respons...
method constructor (line 26) | constructor(n,r){super(r);Qt(this,"name","ResponseError");this.respons...
class zd (line 26) | class zd extends Error{constructor(n,r){super(r);Qt(this,"name","FetchEr...
method constructor (line 26) | constructor(n,r){super(r);Qt(this,"name","FetchError");this.cause=n}
class tn (line 26) | class tn extends Error{constructor(n,r){super(r);Qt(this,"name","Require...
method constructor (line 26) | constructor(n,r){super(r);Qt(this,"name","RequiredError");this.field=n}
function Fl (line 26) | function Fl(e,t=""){return Object.keys(e).map(n=>$l(n,e[n],t)).filter(n=...
function $l (line 26) | function $l(e,t,n=""){const r=n+(n.length?`[${e}]`:e);if(t instanceof Ar...
class Ct (line 26) | class Ct{constructor(t,n=r=>r){this.raw=t,this.transformer=n}async value...
method constructor (line 26) | constructor(t,n=r=>r){this.raw=t,this.transformer=n}
method value (line 26) | async value(){return this.transformer(await this.raw.json())}
class bs (line 26) | class bs{constructor(t){this.raw=t}async value(){}}
method constructor (line 26) | constructor(t){this.raw=t}
method value (line 26) | async value(){}
function Ta (line 26) | function Ta(e){return Qd(e,!1)}
function Qd (line 26) | function Qd(e,t=!1){return e==null?e:{tag_id:e.tagId}}
function Zd (line 26) | function Zd(e){return eh(e,!1)}
function eh (line 26) | function eh(e,t=!1){return e==null?e:{bookmark_ids:e.bookmarkIds,tag_ids...
function th (line 26) | function th(e){return nh(e,!1)}
function nh (line 26) | function nh(e,t=!1){return e==null?e:{password:e.password,remember_me:e....
function wa (line 26) | function wa(e){return rh(e)}
function rh (line 26) | function rh(e,t){return e==null?e:{expires:e.expires==null?void 0:e.expi...
function sh (line 26) | function sh(e){return oh(e)}
function oh (line 26) | function oh(e,t){return e==null?e:{content:e.content==null?void 0:e.cont...
function ah (line 26) | function ah(e){return ih(e)}
function ih (line 26) | function ih(e,t){return e==null?e:{createEbook:e.createEbook==null?void ...
function lh (line 26) | function lh(e){return ch(e,!1)}
function ch (line 26) | function ch(e,t=!1){return e==null?e:{createEbook:e.createEbook,hideExce...
function uh (line 26) | function uh(e){return fh(e,!1)}
function fh (line 26) | function fh(e,t=!1){return e==null?e:{config:lh(e.config),new_password:e...
function dh (line 26) | function dh(e){return hh(e,!1)}
function hh (line 26) | function hh(e,t=!1){return e==null?e:{create_archive:e.createArchive,cre...
function Sa (line 26) | function Sa(e){return mh(e)}
function mh (line 26) | function mh(e,t){return e==null?e:{config:e.config==null?void 0:ah(e.con...
function Ul (line 26) | function Ul(e){return gh(e)}
function gh (line 26) | function gh(e,t){return e==null?e:{bookmarkCount:e.bookmark_count==null?...
function $_ (line 26) | function $_(e){return ph(e,!1)}
function ph (line 26) | function ph(e,t=!1){return e==null?e:{bookmark_count:e.bookmarkCount,del...
function La (line 26) | function La(e){return _h(e)}
function _h (line 26) | function _h(e,t){return e==null?e:{author:e.author==null?void 0:e.author...
class bh (line 26) | class bh extends $s{async apiV1AuthAccountPatchRaw(t,n){const r={},s={};...
method apiV1AuthAccountPatchRaw (line 26) | async apiV1AuthAccountPatchRaw(t,n){const r={},s={};s["Content-Type"]=...
method apiV1AuthAccountPatch (line 26) | async apiV1AuthAccountPatch(t={},n){return await(await this.apiV1AuthA...
method apiV1AuthLoginPostRaw (line 26) | async apiV1AuthLoginPostRaw(t,n){const r={},s={};s["Content-Type"]="ap...
method apiV1AuthLoginPost (line 26) | async apiV1AuthLoginPost(t={},n){return await(await this.apiV1AuthLogi...
method apiV1AuthLogoutPostRaw (line 26) | async apiV1AuthLogoutPostRaw(t){const n={},r={},s=await this.request({...
method apiV1AuthLogoutPost (line 26) | async apiV1AuthLogoutPost(t){await this.apiV1AuthLogoutPostRaw(t)}
method apiV1AuthMeGetRaw (line 26) | async apiV1AuthMeGetRaw(t){const n={},r={},s=await this.request({path:...
method apiV1AuthMeGet (line 26) | async apiV1AuthMeGet(t){return await(await this.apiV1AuthMeGetRaw(t))....
method apiV1AuthRefreshPostRaw (line 26) | async apiV1AuthRefreshPostRaw(t){const n={},r={},s=await this.request(...
method apiV1AuthRefreshPost (line 26) | async apiV1AuthRefreshPost(t){return await(await this.apiV1AuthRefresh...
method apiV1BookmarksBulkTagsPutRaw (line 26) | async apiV1BookmarksBulkTagsPutRaw(t,n){if(t.payload==null)throw new t...
method apiV1BookmarksBulkTagsPut (line 26) | async apiV1BookmarksBulkTagsPut(t,n){return await(await this.apiV1Book...
method apiV1BookmarksCachePutRaw (line 26) | async apiV1BookmarksCachePutRaw(t,n){if(t.payload==null)throw new tn("...
method apiV1BookmarksCachePut (line 26) | async apiV1BookmarksCachePut(t,n){return await(await this.apiV1Bookmar...
method apiV1BookmarksIdReadableGetRaw (line 26) | async apiV1BookmarksIdReadableGetRaw(t){const n={},r={},s=await this.r...
method apiV1BookmarksIdReadableGet (line 26) | async apiV1BookmarksIdReadableGet(t){return await(await this.apiV1Book...
method apiV1BookmarksIdTagsDeleteRaw (line 26) | async apiV1BookmarksIdTagsDeleteRaw(t,n){if(t.id==null)throw new tn("i...
method apiV1BookmarksIdTagsDelete (line 26) | async apiV1BookmarksIdTagsDelete(t,n){await this.apiV1BookmarksIdTagsD...
method apiV1BookmarksIdTagsGetRaw (line 26) | async apiV1BookmarksIdTagsGetRaw(t,n){if(t.id==null)throw new tn("id",...
method apiV1BookmarksIdTagsGet (line 26) | async apiV1BookmarksIdTagsGet(t,n){return await(await this.apiV1Bookma...
method apiV1BookmarksIdTagsPostRaw (line 26) | async apiV1BookmarksIdTagsPostRaw(t,n){if(t.id==null)throw new tn("id"...
method apiV1BookmarksIdTagsPost (line 26) | async apiV1BookmarksIdTagsPost(t,n){await this.apiV1BookmarksIdTagsPos...
method setup (line 26) | setup(e){const t=ur();Xr();const n=ge(!0);return qt(async()=>{if(t.token...
function o (line 26) | function o(a){const i=new Event("vite:preloadError",{cancelable:!0});if(...
function Aa (line 30) | function Aa(e){return e.replace(/</g,"<").replace(/>/g,">").replac...
function ct (line 30) | function ct(e,t){return Ih.call(e,t)}
function Rh (line 30) | function Rh(e,t=""){return e.reduce((n,r,s)=>s===0?n+r:n+t+r,"")}
function Qr (line 30) | function Qr(e){let t=e;return()=>++t}
function Ch (line 30) | function Ch(e,t){typeof console<"u"&&(console.warn("[intlify] "+e),t&&co...
function kr (line 30) | function kr(e,t){if(gr(e)||gr(t))throw new Error("Invalid value");const ...
function Nh (line 34) | function Nh(e,t,n){return{line:e,column:t,offset:n}}
function Cr (line 34) | function Cr(e,t,n){return{start:e,end:t}}
function Wl (line 34) | function Wl(e,...t){return t.length===1&&Mh(t[0])&&(t=t[0]),(!t||!t.hasO...
function Kl (line 34) | function Kl(e,t=""){return e.reduce((n,r,s)=>s===0?n+r:n+t+r,"")}
function Fh (line 34) | function Fh(e,t,...n){const r=Wl(Dh[e],...n||[]),s={message:String(r),co...
function Cn (line 34) | function Cn(e,t,n={}){const{domain:r,messages:s,args:o}=n,a=Wl((s||$h)[e...
function Uh (line 34) | function Uh(e){throw e}
function Bh (line 35) | function Bh(e){const t=e;let n=0,r=1,s=1,o=0;const a=D=>t[D]===Vh&&t[D+1...
function jh (line 35) | function jh(e,t={}){const n=t.location!==!1,r=Bh(e),s=()=>r.index(),o=()...
function Jh (line 35) | function Jh(e,t,n){switch(e){case"\\\\":return"\\";case"\\'":return"'";d...
function Xh (line 35) | function Xh(e={}){const t=e.location!==!1,{onError:n,onWarn:r}=e;functio...
function lt (line 35) | function lt(e){if(e.type===14)return"EOF";const t=(e.value||"").replace(...
function zh (line 35) | function zh(e,t={}){const n={ast:e,helpers:new Set};return{context:()=>n...
function Ca (line 35) | function Ca(e,t){for(let n=0;n<e.length;n++)bo(e[n],t)}
function bo (line 35) | function bo(e,t){switch(e.type){case 1:Ca(e.cases,t),t.helper("plural");...
function Qh (line 35) | function Qh(e,t={}){const n=zh(e);n.helper("normalize"),e.body&&bo(e.bod...
function Zh (line 35) | function Zh(e){const t=e.body;return t.type===2?Na(t):t.cases.forEach(n=...
function Na (line 35) | function Na(e){if(e.items.length===1){const t=e.items[0];(t.type===3||t....
function pn (line 35) | function pn(e){switch(e.t=e.type,e.type){case 0:{const t=e;pn(t.body),t....
function nm (line 35) | function nm(e,t){const{filename:n,breakLineCode:r,needIndent:s}=t,o=t.lo...
function rm (line 35) | function rm(e,t){const{helper:n}=e;e.push(`${n("linked")}(`),In(e,t.key)...
function sm (line 35) | function sm(e,t){const{helper:n,needIndent:r}=e;e.push(`${n("normalize")...
function om (line 35) | function om(e,t){const{helper:n,needIndent:r}=e;if(t.cases.length>1){e.p...
function am (line 35) | function am(e,t){t.body?In(e,t.body):e.push("null")}
function In (line 35) | function In(e,t){const{helper:n}=e;switch(t.type){case 0:am(e,t);break;c...
function lm (line 36) | function lm(e,t={}){const n=Bl({},t),r=!!n.jit,s=!!n.minify,o=n.optimize...
function cm (line 40) | function cm(){typeof __INTLIFY_PROD_DEVTOOLS__!="boolean"&&(At().__INTLI...
function fm (line 40) | function fm(e){return um.test(e)}
function dm (line 40) | function dm(e){const t=e.charCodeAt(0),n=e.charCodeAt(e.length-1);return...
function hm (line 40) | function hm(e){if(e==null)return"o";switch(e.charCodeAt(0)){case 91:case...
function mm (line 40) | function mm(e){const t=e.trim();return e.charAt(0)==="0"&&isNaN(parseInt...
function gm (line 40) | function gm(e){const t=[];let n=-1,r=0,s=0,o,a,i,l,c,u,f;const m=[];m[0]...
function pm (line 40) | function pm(e,t){return ce(e)?e[t]:null}
function _m (line 40) | function _m(e,t){if(!ce(e))return null;let n=xa.get(t);if(n||(n=gm(t),n&...
function Ma (line 40) | function Ma(e,t){return e=Math.abs(e),t===2?e?e>1?1:0:1:e?Math.min(e,2):0}
function Tm (line 40) | function Tm(e){const t=Ae(e.pluralIndex)?e.pluralIndex:-1;return e.named...
function wm (line 40) | function wm(e,t){t.count||(t.count=e),t.n||(t.n=e)}
function Sm (line 40) | function Sm(e={}){const t=e.locale,n=Tm(e),r=ce(e.pluralRules)&&G(t)&&ye...
function Lm (line 40) | function Lm(e){nr=e}
function Om (line 40) | function Om(e,t,n){nr&&nr.emit("i18n:init",{timestamp:Date.now(),i18n:e,...
function Am (line 40) | function Am(e){return t=>nr&&nr.emit(e,t)}
function yt (line 40) | function yt(e){return Cn(e,null,void 0)}
function yo (line 40) | function yo(e,t){return t.locale!=null?Da(t.locale):Da(e.locale)}
function Da (line 40) | function Da(e){if(G(e))return e;if(ye(e)){if(e.resolvedOnce&&ys!=null)re...
function Cm (line 40) | function Cm(e,t,n){return[...new Set([n,...ke(t)?t:ce(t)?Object.keys(t):...
function jl (line 40) | function jl(e,t,n){const r=G(n)?n:An,s=e;s.__localeChainCache||(s.__loca...
function Fa (line 40) | function Fa(e,t,n){let r=!0;for(let s=0;s<t.length&&ne(r);s++){const o=t...
function Nm (line 40) | function Nm(e,t,n){let r;const s=t.split("-");do{const o=s.join("-");r=x...
function xm (line 40) | function xm(e,t,n){let r=!1;if(!e.includes(t)&&(r=!0,t)){r=t[t.length-1]...
function Dm (line 40) | function Dm(){return{upper:(e,t)=>t==="text"&&G(e)?e.toUpperCase():t==="...
function Va (line 40) | function Va(e){ql=e}
function Fm (line 40) | function Fm(e){Yl=e}
function $m (line 40) | function $m(e){Jl=e}
function Wm (line 40) | function Wm(e={}){const t=ye(e.onWarn)?e.onWarn:Ch,n=G(e.version)?e.vers...
function vo (line 40) | function vo(e,t,n,r,s){const{missing:o,onWarn:a}=e;if(o!==null){const i=...
function Dn (line 40) | function Dn(e,t,n){const r=e;r.__localeChainCache=new Map,e.localeFallba...
function Bm (line 40) | function Bm(e,t){return e===t?!1:e.split("-")[0]===t.split("-")[0]}
function Km (line 40) | function Km(e,t){const n=t.indexOf(e);if(n===-1)return!1;for(let r=n+1;r...
function Es (line 40) | function Es(e){return n=>Gm(n,e)}
function Gm (line 40) | function Gm(e,t){const n=qm(t);if(n==null)throw rr(0);if(Eo(n)===1){cons...
function qm (line 40) | function qm(e){return Xt(e,jm)}
function Jm (line 40) | function Jm(e){return Xt(e,Ym,[])}
function Ba (line 40) | function Ba(e,t){const n=zm(t);if(n!=null)return e.type==="text"?n:e.nor...
function zm (line 40) | function zm(e){return Xt(e,Xm)}
function Zm (line 40) | function Zm(e){return Xt(e,Qm,[])}
function Us (line 40) | function Us(e,t){const n=Eo(t);switch(n){case 3:return pr(t,n);case 9:re...
function Eo (line 40) | function Eo(e){return Xt(e,eg)}
function pr (line 40) | function pr(e,t){const n=Xt(e,tg);if(n)return n;throw rr(t)}
function rg (line 40) | function rg(e){return Xt(e,ng)}
function og (line 40) | function og(e){const t=Xt(e,sg);if(t)return t;throw rr(6)}
function Xt (line 40) | function Xt(e,t,n){for(let r=0;r<t.length;r++){const s=t[r];if(ct(e,s)&&...
function rr (line 40) | function rr(e){return new Error(`unhandled node type: ${e}`)}
function Pn (line 40) | function Pn(e){return ce(e)&&Eo(e)===0&&(ct(e,"b")||ct(e,"body"))}
function Zl (line 40) | function Zl(e,t={}){let n=!1;const r=t.onError||Uh;return t.onError=s=>{...
function ig (line 40) | function ig(e,t){if(__INTLIFY_JIT_COMPILATION__&&!__INTLIFY_DROP_MESSAGE...
function Ga (line 40) | function Ga(e,...t){const{fallbackFormat:n,postTranslation:r,unresolving...
function lg (line 40) | function lg(e){ke(e.list)?e.list=e.list.map(t=>G(t)?Aa(t):t):ce(e.named)...
function ec (line 40) | function ec(e,t,n,r,s,o){const{messages:a,onWarn:i,messageResolver:l,loc...
function tc (line 40) | function tc(e,t,n,r,s,o){const{messageCompiler:a,warnHtmlMessage:i}=e;if...
function cg (line 40) | function cg(e,t,n){return t(n)}
function Vs (line 40) | function Vs(...e){const[t,n,r]=e,s=pe();if(!G(t)&&!Ae(t)&&!ot(t)&&!Pn(t)...
function ug (line 40) | function ug(e,t,n,r,s,o){return{locale:t,key:n,warnHtmlMessage:s,onError...
function fg (line 40) | function fg(e,t,n,r){const{modifiers:s,pluralRules:o,messageResolver:a,f...
function ja (line 40) | function ja(e,...t){const{datetimeFormats:n,unresolving:r,fallbackLocale...
function Hs (line 40) | function Hs(...e){const[t,n,r,s]=e,o=pe();let a=pe(),i;if(G(t)){const l=...
function qa (line 40) | function qa(e,t,n){const r=e;for(const s in n){const o=`${t}__${s}`;r.__...
function Ya (line 40) | function Ya(e,...t){const{numberFormats:n,unresolving:r,fallbackLocale:s...
function Ws (line 40) | function Ws(...e){const[t,n,r,s]=e,o=pe();let a=pe();if(!Ae(t))throw yt(...
function Ja (line 40) | function Ja(e,t,n){const r=e;for(const s in n){const o=`${t}__${s}`;r.__...
function hg (line 44) | function hg(){typeof __VUE_I18N_FULL_INSTALL__!="boolean"&&(At().__VUE_I...
function De (line 44) | function De(e,...t){return Cn(e,null,void 0)}
function sr (line 44) | function sr(e){if(!ce(e))return e;for(const t in e)if(ct(e,t))if(!t.incl...
function es (line 44) | function es(e,t){const{messages:n,__i18n:r,messageResolver:s,flatJson:o}...
function ic (line 44) | function ic(e){return e.type}
function lc (line 44) | function lc(e,t,n){let r=ce(t.messages)?t.messages:pe();"__i18nGlobal"in...
function Xa (line 44) | function Xa(e){return Pe(lr,null,e,0)}
function ei (line 44) | function ei(e){return(t,n,r,s)=>e(n,r,Qn()||void 0,s)}
function ko (line 44) | function ko(e={},t){const{__root:n,__injectWithOption:r}=e,s=n===void 0,...
function _g (line 44) | function _g(e){const t=G(e.locale)?e.locale:An,n=G(e.fallbackLocale)||ke...
function qs (line 44) | function qs(e={},t){{const n=ko(_g(e)),{__extender:r}=e,s={id:n.id,get l...
function bg (line 44) | function bg({slots:e},t){return t.length===1&&t[0]==="default"?(e.defaul...
function cc (line 44) | function cc(e){return Ce}
method setup (line 44) | setup(e,t){const{slots:n,attrs:r}=t,s=e.i18n||fn({useScope:e.scope,__use...
function vg (line 44) | function vg(e){return ke(e)&&!G(e[0])}
function uc (line 44) | function uc(e,t,n,r){const{slots:s,attrs:o}=t;return()=>{const a={part:!...
method setup (line 44) | setup(e,t){const n=e.i18n||fn({useScope:e.scope,__useComponent:!0});retu...
method setup (line 44) | setup(e,t){const n=e.i18n||fn({useScope:e.scope,__useComponent:!0});retu...
function Tg (line 44) | function Tg(e,t){const n=e;if(e.mode==="composition")return n.__getInsta...
function wg (line 44) | function wg(e){const t=a=>{const{instance:i,modifiers:l,value:c}=a;if(!i...
function si (line 44) | function si(e){if(G(e))return{path:e};if(Z(e)){if(!("path"in e))throw De...
function oi (line 44) | function oi(e){const{path:t,locale:n,args:r,choice:s,plural:o}=e,a={},i=...
function Sg (line 44) | function Sg(e,t,...n){const r=Z(n[0])?n[0]:{},s=!!r.useI18nComponentName...
function Lg (line 44) | function Lg(e,t,n){return{beforeCreate(){const r=Qn();if(!r)throw De(Ne....
function ai (line 44) | function ai(e,t){e.locale=t.locale||e.locale,e.fallbackLocale=t.fallback...
function Ig (line 44) | function Ig(e={},t){const n=__VUE_I18N_LEGACY_API__&&ne(e.legacy)?e.lega...
function fn (line 44) | function fn(e={}){const t=Qn();if(t==null)throw De(Ne.MUST_BE_CALL_SETUP...
function Ag (line 44) | function Ag(e,t,n){const r=Zs();{const s=__VUE_I18N_LEGACY_API__&&t?r.ru...
function Pg (line 44) | function Pg(e){{const t=rt(e.isCE?Og:e.appContext.app.__VUE_I18N_SYMBOL_...
function Rg (line 44) | function Rg(e,t){return zr(e)?"__i18n"in t?"local":"global":e.useScope?e...
function Cg (line 44) | function Cg(e){return e.mode==="composition"?e.global:e.global.__composer}
function Ng (line 44) | function Ng(e,t,n=!1){let r=null;const s=t.root;let o=xg(t,n);for(;o!=nu...
function xg (line 44) | function xg(e,t=!1){return e==null?null:t&&e.vnode.ctx||e.parent}
function Mg (line 44) | function Mg(e,t,n){qt(()=>{},t),Rn(()=>{const r=n;e.__deleteInstance(t);...
function Dg (line 44) | function Dg(e,t,n,r={}){const s=t==="local",o=lo(null);if(s&&e.proxy&&!(...
function $g (line 44) | function $g(e,t){const n=Object.create(null);return Fg.forEach(s=>{const...
method setup (line 44) | setup(e){const{t}=fn(),n=ur(),r=Xr(),s=ge(!1),o=ge(null),a=[{nameKey:"na...
method setup (line 58) | setup(e){const{t,locale:n}=fn(),r=[{code:"en",name:"English"},{code:"es"...
method setup (line 58) | setup(e){const{t}=fn(),n=ge(!1),r=ur(),s=Xr(),o=ge(null),a=c=>{c.stopPro...
method setup (line 58) | setup(e){const t=ge(!1),n=()=>{t.value=window.innerWidth<768};return qt(...
method setup (line 58) | setup(e){const t=ge([{id:1,title:"Example Bookmark 1",url:"https://examp...
method setup (line 58) | setup(e){const t=e,{t:n}=fn(),r=ge(""),s=ge(""),o=ge(!1),a=ge(""),i=ge(!...
FILE: webapp/src/client/apis/AccountsApi.ts
type ApiV1AccountsIdDeleteRequest (line 28) | interface ApiV1AccountsIdDeleteRequest {
type ApiV1AccountsIdPatchRequest (line 32) | interface ApiV1AccountsIdPatchRequest {
class AccountsApi (line 40) | class AccountsApi extends runtime.BaseAPI {
method apiV1AccountsGetRaw (line 46) | async apiV1AccountsGetRaw(initOverrides?: RequestInit | runtime.InitOv...
method apiV1AccountsGet (line 65) | async apiV1AccountsGet(initOverrides?: RequestInit | runtime.InitOverr...
method apiV1AccountsIdDeleteRaw (line 73) | async apiV1AccountsIdDeleteRaw(requestParameters: ApiV1AccountsIdDelet...
method apiV1AccountsIdDelete (line 98) | async apiV1AccountsIdDelete(requestParameters: ApiV1AccountsIdDeleteRe...
method apiV1AccountsIdPatchRaw (line 105) | async apiV1AccountsIdPatchRaw(requestParameters: ApiV1AccountsIdPatchR...
method apiV1AccountsIdPatch (line 140) | async apiV1AccountsIdPatch(requestParameters: ApiV1AccountsIdPatchRequ...
method apiV1AccountsPostRaw (line 148) | async apiV1AccountsPostRaw(initOverrides?: RequestInit | runtime.InitO...
method apiV1AccountsPost (line 166) | async apiV1AccountsPost(initOverrides?: RequestInit | runtime.InitOver...
FILE: webapp/src/client/apis/AuthApi.ts
type ApiV1AuthAccountPatchRequest (line 52) | interface ApiV1AuthAccountPatchRequest {
type ApiV1AuthLoginPostRequest (line 56) | interface ApiV1AuthLoginPostRequest {
type ApiV1BookmarksBulkTagsPutRequest (line 60) | interface ApiV1BookmarksBulkTagsPutRequest {
type ApiV1BookmarksCachePutRequest (line 64) | interface ApiV1BookmarksCachePutRequest {
type ApiV1BookmarksIdTagsDeleteRequest (line 68) | interface ApiV1BookmarksIdTagsDeleteRequest {
type ApiV1BookmarksIdTagsGetRequest (line 73) | interface ApiV1BookmarksIdTagsGetRequest {
type ApiV1BookmarksIdTagsPostRequest (line 77) | interface ApiV1BookmarksIdTagsPostRequest {
class AuthApi (line 85) | class AuthApi extends runtime.BaseAPI {
method apiV1AuthAccountPatchRaw (line 90) | async apiV1AuthAccountPatchRaw(requestParameters: ApiV1AuthAccountPatc...
method apiV1AuthAccountPatch (line 111) | async apiV1AuthAccountPatch(requestParameters: ApiV1AuthAccountPatchRe...
method apiV1AuthLoginPostRaw (line 119) | async apiV1AuthLoginPostRaw(requestParameters: ApiV1AuthLoginPostReque...
method apiV1AuthLoginPost (line 140) | async apiV1AuthLoginPost(requestParameters: ApiV1AuthLoginPostRequest ...
method apiV1AuthLogoutPostRaw (line 148) | async apiV1AuthLogoutPostRaw(initOverrides?: RequestInit | runtime.Ini...
method apiV1AuthLogoutPost (line 166) | async apiV1AuthLogoutPost(initOverrides?: RequestInit | runtime.InitOv...
method apiV1AuthMeGetRaw (line 173) | async apiV1AuthMeGetRaw(initOverrides?: RequestInit | runtime.InitOver...
method apiV1AuthMeGet (line 191) | async apiV1AuthMeGet(initOverrides?: RequestInit | runtime.InitOverrid...
method apiV1AuthRefreshPostRaw (line 199) | async apiV1AuthRefreshPostRaw(initOverrides?: RequestInit | runtime.In...
method apiV1AuthRefreshPost (line 217) | async apiV1AuthRefreshPost(initOverrides?: RequestInit | runtime.InitO...
method apiV1BookmarksBulkTagsPutRaw (line 225) | async apiV1BookmarksBulkTagsPutRaw(requestParameters: ApiV1BookmarksBu...
method apiV1BookmarksBulkTagsPut (line 253) | async apiV1BookmarksBulkTagsPut(requestParameters: ApiV1BookmarksBulkT...
method apiV1BookmarksCachePutRaw (line 261) | async apiV1BookmarksCachePutRaw(requestParameters: ApiV1BookmarksCache...
method apiV1BookmarksCachePut (line 289) | async apiV1BookmarksCachePut(requestParameters: ApiV1BookmarksCachePut...
method apiV1BookmarksIdReadableGetRaw (line 297) | async apiV1BookmarksIdReadableGetRaw(initOverrides?: RequestInit | run...
method apiV1BookmarksIdReadableGet (line 315) | async apiV1BookmarksIdReadableGet(initOverrides?: RequestInit | runtim...
method apiV1BookmarksIdTagsDeleteRaw (line 323) | async apiV1BookmarksIdTagsDeleteRaw(requestParameters: ApiV1BookmarksI...
method apiV1BookmarksIdTagsDelete (line 358) | async apiV1BookmarksIdTagsDelete(requestParameters: ApiV1BookmarksIdTa...
method apiV1BookmarksIdTagsGetRaw (line 365) | async apiV1BookmarksIdTagsGetRaw(requestParameters: ApiV1BookmarksIdTa...
method apiV1BookmarksIdTagsGet (line 390) | async apiV1BookmarksIdTagsGet(requestParameters: ApiV1BookmarksIdTagsG...
method apiV1BookmarksIdTagsPostRaw (line 398) | async apiV1BookmarksIdTagsPostRaw(requestParameters: ApiV1BookmarksIdT...
method apiV1BookmarksIdTagsPost (line 433) | async apiV1BookmarksIdTagsPost(requestParameters: ApiV1BookmarksIdTags...
FILE: webapp/src/client/apis/SystemApi.ts
class SystemApi (line 28) | class SystemApi extends runtime.BaseAPI {
method apiV1SystemInfoGetRaw (line 34) | async apiV1SystemInfoGetRaw(initOverrides?: RequestInit | runtime.Init...
method apiV1SystemInfoGet (line 53) | async apiV1SystemInfoGet(initOverrides?: RequestInit | runtime.InitOve...
FILE: webapp/src/client/apis/TagsApi.ts
type ApiV1TagsGetRequest (line 25) | interface ApiV1TagsGetRequest {
type ApiV1TagsIdDeleteRequest (line 31) | interface ApiV1TagsIdDeleteRequest {
type ApiV1TagsIdGetRequest (line 35) | interface ApiV1TagsIdGetRequest {
type ApiV1TagsIdPutRequest (line 39) | interface ApiV1TagsIdPutRequest {
type ApiV1TagsPostRequest (line 44) | interface ApiV1TagsPostRequest {
class TagsApi (line 51) | class TagsApi extends runtime.BaseAPI {
method apiV1TagsGetRaw (line 57) | async apiV1TagsGetRaw(requestParameters: ApiV1TagsGetRequest, initOver...
method apiV1TagsGet (line 88) | async apiV1TagsGet(requestParameters: ApiV1TagsGetRequest = {}, initOv...
method apiV1TagsIdDeleteRaw (line 97) | async apiV1TagsIdDeleteRaw(requestParameters: ApiV1TagsIdDeleteRequest...
method apiV1TagsIdDelete (line 123) | async apiV1TagsIdDelete(requestParameters: ApiV1TagsIdDeleteRequest, i...
method apiV1TagsIdGetRaw (line 131) | async apiV1TagsIdGetRaw(requestParameters: ApiV1TagsIdGetRequest, init...
method apiV1TagsIdGet (line 157) | async apiV1TagsIdGet(requestParameters: ApiV1TagsIdGetRequest, initOve...
method apiV1TagsIdPutRaw (line 166) | async apiV1TagsIdPutRaw(requestParameters: ApiV1TagsIdPutRequest, init...
method apiV1TagsIdPut (line 202) | async apiV1TagsIdPut(requestParameters: ApiV1TagsIdPutRequest, initOve...
method apiV1TagsPostRaw (line 211) | async apiV1TagsPostRaw(requestParameters: ApiV1TagsPostRequest, initOv...
method apiV1TagsPost (line 240) | async apiV1TagsPost(requestParameters: ApiV1TagsPostRequest, initOverr...
FILE: webapp/src/client/models/ApiV1BookmarkTagPayload.ts
type ApiV1BookmarkTagPayload (line 21) | interface ApiV1BookmarkTagPayload {
function instanceOfApiV1BookmarkTagPayload (line 33) | function instanceOfApiV1BookmarkTagPayload(value: object): value is ApiV...
function ApiV1BookmarkTagPayloadFromJSON (line 38) | function ApiV1BookmarkTagPayloadFromJSON(json: any): ApiV1BookmarkTagPay...
function ApiV1BookmarkTagPayloadFromJSONTyped (line 42) | function ApiV1BookmarkTagPayloadFromJSONTyped(json: any, ignoreDiscrimin...
function ApiV1BookmarkTagPayloadToJSON (line 52) | function ApiV1BookmarkTagPayloadToJSON(json: any): ApiV1BookmarkTagPaylo...
function ApiV1BookmarkTagPayloadToJSONTyped (line 56) | function ApiV1BookmarkTagPayloadToJSONTyped(value?: ApiV1BookmarkTagPayl...
FILE: webapp/src/client/models/ApiV1BulkUpdateBookmarkTagsPayload.ts
type ApiV1BulkUpdateBookmarkTagsPayload (line 21) | interface ApiV1BulkUpdateBookmarkTagsPayload {
function instanceOfApiV1BulkUpdateBookmarkTagsPayload (line 39) | function instanceOfApiV1BulkUpdateBookmarkTagsPayload(value: object): va...
function ApiV1BulkUpdateBookmarkTagsPayloadFromJSON (line 45) | function ApiV1BulkUpdateBookmarkTagsPayloadFromJSON(json: any): ApiV1Bul...
function ApiV1BulkUpdateBookmarkTagsPayloadFromJSONTyped (line 49) | function ApiV1BulkUpdateBookmarkTagsPayloadFromJSONTyped(json: any, igno...
function ApiV1BulkUpdateBookmarkTagsPayloadToJSON (line 60) | function ApiV1BulkUpdateBookmarkTagsPayloadToJSON(json: any): ApiV1BulkU...
function ApiV1BulkUpdateBookmarkTagsPayloadToJSONTyped (line 64) | function ApiV1BulkUpdateBookmarkTagsPayloadToJSONTyped(value?: ApiV1Bulk...
FILE: webapp/src/client/models/ApiV1InfoResponse.ts
type ApiV1InfoResponse (line 29) | interface ApiV1InfoResponse {
function instanceOfApiV1InfoResponse (line 53) | function instanceOfApiV1InfoResponse(value: object): value is ApiV1InfoR...
function ApiV1InfoResponseFromJSON (line 57) | function ApiV1InfoResponseFromJSON(json: any): ApiV1InfoResponse {
function ApiV1InfoResponseFromJSONTyped (line 61) | function ApiV1InfoResponseFromJSONTyped(json: any, ignoreDiscriminator: ...
function ApiV1InfoResponseToJSON (line 73) | function ApiV1InfoResponseToJSON(json: any): ApiV1InfoResponse {
function ApiV1InfoResponseToJSONTyped (line 77) | function ApiV1InfoResponseToJSONTyped(value?: ApiV1InfoResponse | null, ...
FILE: webapp/src/client/models/ApiV1InfoResponseVersion.ts
type ApiV1InfoResponseVersion (line 21) | interface ApiV1InfoResponseVersion {
function instanceOfApiV1InfoResponseVersion (line 45) | function instanceOfApiV1InfoResponseVersion(value: object): value is Api...
function ApiV1InfoResponseVersionFromJSON (line 49) | function ApiV1InfoResponseVersionFromJSON(json: any): ApiV1InfoResponseV...
function ApiV1InfoResponseVersionFromJSONTyped (line 53) | function ApiV1InfoResponseVersionFromJSONTyped(json: any, ignoreDiscrimi...
function ApiV1InfoResponseVersionToJSON (line 65) | function ApiV1InfoResponseVersionToJSON(json: any): ApiV1InfoResponseVer...
function ApiV1InfoResponseVersionToJSONTyped (line 69) | function ApiV1InfoResponseVersionToJSONTyped(value?: ApiV1InfoResponseVe...
FILE: webapp/src/client/models/ApiV1LoginRequestPayload.ts
type ApiV1LoginRequestPayload (line 21) | interface ApiV1LoginRequestPayload {
function instanceOfApiV1LoginRequestPayload (line 45) | function instanceOfApiV1LoginRequestPayload(value: object): value is Api...
function ApiV1LoginRequestPayloadFromJSON (line 49) | function ApiV1LoginRequestPayloadFromJSON(json: any): ApiV1LoginRequestP...
function ApiV1LoginRequestPayloadFromJSONTyped (line 53) | function ApiV1LoginRequestPayloadFromJSONTyped(json: any, ignoreDiscrimi...
function ApiV1LoginRequestPayloadToJSON (line 65) | function ApiV1LoginRequestPayloadToJSON(json: any): ApiV1LoginRequestPay...
function ApiV1LoginRequestPayloadToJSONTyped (line 69) | function ApiV1LoginRequestPayloadToJSONTyped(value?: ApiV1LoginRequestPa...
FILE: webapp/src/client/models/ApiV1LoginResponseMessage.ts
type ApiV1LoginResponseMessage (line 21) | interface ApiV1LoginResponseMessage {
function instanceOfApiV1LoginResponseMessage (line 39) | function instanceOfApiV1LoginResponseMessage(value: object): value is Ap...
function ApiV1LoginResponseMessageFromJSON (line 43) | function ApiV1LoginResponseMessageFromJSON(json: any): ApiV1LoginRespons...
function ApiV1LoginResponseMessageFromJSONTyped (line 47) | function ApiV1LoginResponseMessageFromJSONTyped(json: any, ignoreDiscrim...
function ApiV1LoginResponseMessageToJSON (line 58) | function ApiV1LoginResponseMessageToJSON(json: any): ApiV1LoginResponseM...
function ApiV1LoginResponseMessageToJSONTyped (line 62) | function ApiV1LoginResponseMessageToJSONTyped(value?: ApiV1LoginResponse...
FILE: webapp/src/client/models/ApiV1ReadableResponseMessage.ts
type ApiV1ReadableResponseMessage (line 21) | interface ApiV1ReadableResponseMessage {
function instanceOfApiV1ReadableResponseMessage (line 39) | function instanceOfApiV1ReadableResponseMessage(value: object): value is...
function ApiV1ReadableResponseMessageFromJSON (line 43) | function ApiV1ReadableResponseMessageFromJSON(json: any): ApiV1ReadableR...
function ApiV1ReadableResponseMessageFromJSONTyped (line 47) | function ApiV1ReadableResponseMessageFromJSONTyped(json: any, ignoreDisc...
function ApiV1ReadableResponseMessageToJSON (line 58) | function ApiV1ReadableResponseMessageToJSON(json: any): ApiV1ReadableRes...
function ApiV1ReadableResponseMessageToJSONTyped (line 62) | function ApiV1ReadableResponseMessageToJSONTyped(value?: ApiV1ReadableRe...
FILE: webapp/src/client/models/ApiV1UpdateAccountPayload.ts
type ApiV1UpdateAccountPayload (line 29) | interface ApiV1UpdateAccountPayload {
function instanceOfApiV1UpdateAccountPayload (line 65) | function instanceOfApiV1UpdateAccountPayload(value: object): value is Ap...
function ApiV1UpdateAccountPayloadFromJSON (line 69) | function ApiV1UpdateAccountPayloadFromJSON(json: any): ApiV1UpdateAccoun...
function ApiV1UpdateAccountPayloadFromJSONTyped (line 73) | function ApiV1UpdateAccountPayloadFromJSONTyped(json: any, ignoreDiscrim...
function ApiV1UpdateAccountPayloadToJSON (line 87) | function ApiV1UpdateAccountPayloadToJSON(json: any): ApiV1UpdateAccountP...
function ApiV1UpdateAccountPayloadToJSONTyped (line 91) | function ApiV1UpdateAccountPayloadToJSONTyped(value?: ApiV1UpdateAccount...
FILE: webapp/src/client/models/ApiV1UpdateCachePayload.ts
type ApiV1UpdateCachePayload (line 21) | interface ApiV1UpdateCachePayload {
function instanceOfApiV1UpdateCachePayload (line 57) | function instanceOfApiV1UpdateCachePayload(value: object): value is ApiV...
function ApiV1UpdateCachePayloadFromJSON (line 62) | function ApiV1UpdateCachePayloadFromJSON(json: any): ApiV1UpdateCachePay...
function ApiV1UpdateCachePayloadFromJSONTyped (line 66) | function ApiV1UpdateCachePayloadFromJSONTyped(json: any, ignoreDiscrimin...
function ApiV1UpdateCachePayloadToJSON (line 80) | function ApiV1UpdateCachePayloadToJSON(json: any): ApiV1UpdateCachePaylo...
function ApiV1UpdateCachePayloadToJSONTyped (line 84) | function ApiV1UpdateCachePayloadToJSONTyped(value?: ApiV1UpdateCachePayl...
FILE: webapp/src/client/models/ModelAccount.ts
type ModelAccount (line 29) | interface ModelAccount {
function instanceOfModelAccount (line 65) | function instanceOfModelAccount(value: object): value is ModelAccount {
function ModelAccountFromJSON (line 69) | function ModelAccountFromJSON(json: any): ModelAccount {
function ModelAccountFromJSONTyped (line 73) | function ModelAccountFromJSONTyped(json: any, ignoreDiscriminator: boole...
function ModelAccountToJSON (line 87) | function ModelAccountToJSON(json: any): ModelAccount {
function ModelAccountToJSONTyped (line 91) | function ModelAccountToJSONTyped(value?: ModelAccount | null, ignoreDisc...
FILE: webapp/src/client/models/ModelAccountDTO.ts
type ModelAccountDTO (line 29) | interface ModelAccountDTO {
function instanceOfModelAccountDTO (line 65) | function instanceOfModelAccountDTO(value: object): value is ModelAccount...
function ModelAccountDTOFromJSON (line 69) | function ModelAccountDTOFromJSON(json: any): ModelAccountDTO {
function ModelAccountDTOFromJSONTyped (line 73) | function ModelAccountDTOFromJSONTyped(json: any, ignoreDiscriminator: bo...
function ModelAccountDTOToJSON (line 87) | function ModelAccountDTOToJSON(json: any): ModelAccountDTO {
function ModelAccountDTOToJSONTyped (line 91) | function ModelAccountDTOToJSONTyped(value?: ModelAccountDTO | null, igno...
FILE: webapp/src/client/models/ModelBookmarkDTO.ts
type ModelBookmarkDTO (line 29) | interface ModelBookmarkDTO {
function instanceOfModelBookmarkDTO (line 131) | function instanceOfModelBookmarkDTO(value: object): value is ModelBookma...
function ModelBookmarkDTOFromJSON (line 135) | function ModelBookmarkDTOFromJSON(json: any): ModelBookmarkDTO {
function ModelBookmarkDTOFromJSONTyped (line 139) | function ModelBookmarkDTOFromJSONTyped(json: any, ignoreDiscriminator: b...
function ModelBookmarkDTOToJSON (line 164) | function ModelBookmarkDTOToJSON(json: any): ModelBookmarkDTO {
function ModelBookmarkDTOToJSONTyped (line 168) | function ModelBookmarkDTOToJSONTyped(value?: ModelBookmarkDTO | null, ig...
FILE: webapp/src/client/models/ModelTagDTO.ts
type ModelTagDTO (line 21) | interface ModelTagDTO {
function instanceOfModelTagDTO (line 51) | function instanceOfModelTagDTO(value: object): value is ModelTagDTO {
function ModelTagDTOFromJSON (line 55) | function ModelTagDTOFromJSON(json: any): ModelTagDTO {
function ModelTagDTOFromJSONTyped (line 59) | function ModelTagDTOFromJSONTyped(json: any, ignoreDiscriminator: boolea...
function ModelTagDTOToJSON (line 72) | function ModelTagDTOToJSON(json: any): ModelTagDTO {
function ModelTagDTOToJSONTyped (line 76) | function ModelTagDTOToJSONTyped(value?: ModelTagDTO | null, ignoreDiscri...
FILE: webapp/src/client/models/ModelUserConfig.ts
type ModelUserConfig (line 21) | interface ModelUserConfig {
function instanceOfModelUserConfig (line 81) | function instanceOfModelUserConfig(value: object): value is ModelUserCon...
function ModelUserConfigFromJSON (line 85) | function ModelUserConfigFromJSON(json: any): ModelUserConfig {
function ModelUserConfigFromJSONTyped (line 89) | function ModelUserConfigFromJSONTyped(json: any, ignoreDiscriminator: bo...
function ModelUserConfigToJSON (line 107) | function ModelUserConfigToJSON(json: any): ModelUserConfig {
function ModelUserConfigToJSONTyped (line 111) | function ModelUserConfigToJSONTyped(value?: ModelUserConfig | null, igno...
FILE: webapp/src/client/runtime.ts
constant BASE_PATH (line 16) | const BASE_PATH = "http://localhost".replace(/\/+$/, "");
type ConfigurationParameters (line 18) | interface ConfigurationParameters {
class Configuration (line 31) | class Configuration {
method constructor (line 32) | constructor(private configuration: ConfigurationParameters = {}) {}
method config (line 34) | set config(configuration: Configuration) {
method basePath (line 38) | get basePath(): string {
method fetchApi (line 42) | get fetchApi(): FetchAPI | undefined {
method middleware (line 46) | get middleware(): Middleware[] {
method queryParamsStringify (line 50) | get queryParamsStringify(): (params: HTTPQuery) => string {
method username (line 54) | get username(): string | undefined {
method password (line 58) | get password(): string | undefined {
method apiKey (line 62) | get apiKey(): ((name: string) => string | Promise<string>) | undefined {
method accessToken (line 70) | get accessToken(): ((name?: string, scopes?: string[]) => string | Pro...
method headers (line 78) | get headers(): HTTPHeaders | undefined {
method credentials (line 82) | get credentials(): RequestCredentials | undefined {
class BaseAPI (line 92) | class BaseAPI {
method constructor (line 97) | constructor(protected configuration = DefaultConfig) {
method withMiddleware (line 101) | withMiddleware<T extends BaseAPI>(this: T, ...middlewares: Middleware[...
method withPreMiddleware (line 107) | withPreMiddleware<T extends BaseAPI>(this: T, ...preMiddlewares: Array...
method withPostMiddleware (line 112) | withPostMiddleware<T extends BaseAPI>(this: T, ...postMiddlewares: Arr...
method isJsonMime (line 127) | protected isJsonMime(mime: string | null | undefined): boolean {
method request (line 134) | protected async request(context: RequestOpts, initOverrides?: RequestI...
method createFetchParams (line 143) | private async createFetchParams(context: RequestOpts, initOverrides?: ...
method clone (line 244) | private clone<T extends BaseAPI>(this: T): T {
function isBlob (line 252) | function isBlob(value: any): value is Blob {
function isFormData (line 256) | function isFormData(value: any): value is FormData {
class ResponseError (line 260) | class ResponseError extends Error {
method constructor (line 262) | constructor(public response: Response, msg?: string) {
class FetchError (line 267) | class FetchError extends Error {
method constructor (line 269) | constructor(public cause: Error, msg?: string) {
class RequiredError (line 274) | class RequiredError extends Error {
method constructor (line 276) | constructor(public field: string, msg?: string) {
constant COLLECTION_FORMATS (line 281) | const COLLECTION_FORMATS = {
type FetchAPI (line 288) | type FetchAPI = WindowOrWorkerGlobalScope['fetch'];
type Json (line 290) | type Json = any;
type HTTPMethod (line 291) | type HTTPMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'OPTIONS...
type HTTPHeaders (line 292) | type HTTPHeaders = { [key: string]: string };
type HTTPQuery (line 293) | type HTTPQuery = { [key: string]: string | number | null | boolean | Arr...
type HTTPBody (line 294) | type HTTPBody = Json | FormData | URLSearchParams;
type HTTPRequestInit (line 295) | type HTTPRequestInit = { headers?: HTTPHeaders; method: HTTPMethod; cred...
type ModelPropertyNaming (line 296) | type ModelPropertyNaming = 'camelCase' | 'snake_case' | 'PascalCase' | '...
type InitOverrideFunction (line 298) | type InitOverrideFunction = (requestContext: { init: HTTPRequestInit, co...
type FetchParams (line 300) | interface FetchParams {
type RequestOpts (line 305) | interface RequestOpts {
function querystring (line 313) | function querystring(params: HTTPQuery, prefix: string = ''): string {
function querystringSingleKey (line 320) | function querystringSingleKey(key: string, value: string | number | null...
function exists (line 340) | function exists(json: any, key: string) {
function mapValues (line 345) | function mapValues(data: any, fn: (item: any) => any) {
function canConsumeForm (line 352) | function canConsumeForm(consumes: Consume[]): boolean {
type Consume (line 361) | interface Consume {
type RequestContext (line 365) | interface RequestContext {
type ResponseContext (line 371) | interface ResponseContext {
type ErrorContext (line 378) | interface ErrorContext {
type Middleware (line 386) | interface Middleware {
type ApiResponse (line 392) | interface ApiResponse<T> {
type ResponseTransformer (line 397) | interface ResponseTransformer<T> {
class JSONApiResponse (line 401) | class JSONApiResponse<T> {
method constructor (line 402) | constructor(public raw: Response, private transformer: ResponseTransfo...
method value (line 404) | async value(): Promise<T> {
class VoidApiResponse (line 409) | class VoidApiResponse {
method constructor (line 410) | constructor(public raw: Response) {}
method value (line 412) | async value(): Promise<void> {
class BlobApiResponse (line 417) | class BlobApiResponse {
method constructor (line 418) | constructor(public raw: Response) {}
method value (line 420) | async value(): Promise<Blob> {
class TextApiResponse (line 425) | class TextApiResponse {
method constructor (line 426) | constructor(public raw: Response) {}
method value (line 428) | async value(): Promise<string> {
FILE: webapp/src/utils/i18n.ts
type SupportedLocale (line 9) | type SupportedLocale = 'en' | 'es' | 'fr' | 'de' | 'ja';
Condensed preview — 328 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (1,931K chars).
[
{
"path": ".cursorrules",
"chars": 173,
"preview": "# Shiori Test Commands\n\n# Run the entire test suite\nmake unittest\n\n# Run SQLite database tests only\ngo test -timeout 10s"
},
{
"path": ".dockerignore",
"chars": 49,
"preview": "dev-data*\ndocs\n!docs/swagger\nDockerfile\n*.md\n/.*\n"
},
{
"path": ".github/ISSUE_TEMPLATE/bug_report.md",
"chars": 909,
"preview": "---\nname: Bug report\nabout: Report problems with Shiori\ntitle: One line description of the bug\nlabels: type:bug\nassignee"
},
{
"path": ".github/ISSUE_TEMPLATE/feature_request.md",
"chars": 521,
"preview": "---\nname: Feature request\nabout: PLEASE READ ISSUE\ntitle: ''\nlabels: ''\nassignees: ''\n---\n\nPlease report feature request"
},
{
"path": ".github/dependabot.yml",
"chars": 511,
"preview": "# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates\n\nversion: 2\nupd"
},
{
"path": ".github/stale.yml",
"chars": 833,
"preview": "# Number of days of inactivity before an issue becomes stale\ndaysUntilStale: 30\n# Number of days of inactivity before a "
},
{
"path": ".github/workflows/_buildx.yml",
"chars": 2778,
"preview": "name: \"Build Docker\"\n\non:\n workflow_call:\n secrets:\n DOCKERHUB_USERNAME:\n required: true\n DOCKERHUB"
},
{
"path": ".github/workflows/_delete-registry-tag.yml",
"chars": 546,
"preview": "name: Delete registry tag\n\non:\n workflow_call:\n inputs:\n tag_name:\n description: 'The docker tag to remo"
},
{
"path": ".github/workflows/_e2e.yml",
"chars": 760,
"preview": "name: \"E2E Tests\"\n\non: workflow_call\n\njobs:\n e2e-tests:\n runs-on: ubuntu-latest\n\n name: Tests\n steps:\n - us"
},
{
"path": ".github/workflows/_golangci-lint.yml",
"chars": 1097,
"preview": "name: \"golangci-lint\"\n\non: workflow_call\n\npermissions:\n contents: read\n pull-requests: read\njobs:\n golangci:\n runs"
},
{
"path": ".github/workflows/_gorelease.yml",
"chars": 994,
"preview": "name: \"Goreleaser\"\n\non: workflow_call\n\npermissions:\n contents: write\n\njobs:\n goreleaser:\n runs-on: ubuntu-latest\n "
},
{
"path": ".github/workflows/_mkdocs-check.yml",
"chars": 406,
"preview": "name: \"Check mkdocs documentation\"\n\non: workflow_call\n\njobs:\n mkdocs-check:\n runs-on: ubuntu-latest\n steps:\n "
},
{
"path": ".github/workflows/_mkdocs-publish.yml",
"chars": 400,
"preview": "name: \"Publish documentation\"\n\non: workflow_call\n\npermissions:\n contents: write\n\njobs:\n deploy:\n runs-on: ubuntu-la"
},
{
"path": ".github/workflows/_styles-check.yml",
"chars": 383,
"preview": "name: \"styles-check\"\n\non: workflow_call\n\njobs:\n styles-check:\n runs-on: ubuntu-latest\n steps:\n - uses: actio"
},
{
"path": ".github/workflows/_swagger-check.yml",
"chars": 562,
"preview": "name: \"swagger-check\"\n\non: workflow_call\n\njobs:\n swagger-check:\n runs-on: ubuntu-latest\n steps:\n - uses: act"
},
{
"path": ".github/workflows/_test.yml",
"chars": 5027,
"preview": "name: \"Unit Tests\"\n\non:\n workflow_call:\n secrets:\n CODECOV_TOKEN:\n required: true\n\nenv:\n CGO_ENABLED: 0"
},
{
"path": ".github/workflows/pull_request.yml",
"chars": 1387,
"preview": "name: 'Pull Request'\n\non:\n pull_request:\n branches:\n - master\n\nconcurrency:\n group: ci-tests-${{ github.ref }}-1"
},
{
"path": ".github/workflows/pull_request_closed.yml",
"chars": 287,
"preview": "name: 'Clean up Docker images from PR'\n\non:\n pull_request:\n types:\n - closed\n\njobs:\n delete-tag:\n uses: ./.gi"
},
{
"path": ".github/workflows/push.yml",
"chars": 1061,
"preview": "name: 'Push'\n\non:\n push:\n branches:\n - \"master\"\n tags:\n - \"v*\"\n\nconcurrency:\n group: ci-tests-${{ gith"
},
{
"path": ".github/workflows/version_bump.yml",
"chars": 1606,
"preview": "name: \"Tag release\"\n\non:\n workflow_dispatch:\n inputs:\n version:\n description: \"Version to bump to, examp"
},
{
"path": ".gitignore",
"chars": 347,
"preview": "# Exclude IDE\n.vscode/\n.idea/\n\n# Exclude config file\n*.toml\n\n# Exclude executable file\n/shiori*\n\n# Exclude development d"
},
{
"path": ".golangci.bck.yml",
"chars": 526,
"preview": "# Docs: https://golangci-lint.run/usage/configuration/#config-file\nrun:\n timeout: 5m\n\nissues:\n max-issues-per-linter: "
},
{
"path": ".golangci.yml",
"chars": 718,
"preview": "version: \"2\"\nlinters:\n default: none\n enable:\n - copyloopvar\n - ineffassign\n - predeclared\n - staticcheck\n"
},
{
"path": ".goreleaser.yaml",
"chars": 1940,
"preview": "version: 2\n\nbefore:\n hooks:\n - go mod tidy\n\ngit:\n ignore_tags:\n - \"{{ if not .IsNightly }}*-rc*{{ end }}\"\n\nbuild"
},
{
"path": ".prettierignore",
"chars": 328,
"preview": "# Ignore some files we don't want to format\n*.html\n*.json\n*.md\n*.yml\n*.yaml\n\n# Ignore build artifacts\ninternal/view/asse"
},
{
"path": ".prettierrc",
"chars": 22,
"preview": "{\n \"useTabs\": true\n}\n"
},
{
"path": "CODE_OF_CONDUCT.md",
"chars": 992,
"preview": "# Community Conduct Guideline\n\nThe following conduct guideline is based on [Ruby's](https://www.ruby-lang.org/en/conduct"
},
{
"path": "Dockerfile",
"chars": 847,
"preview": "# Build stage\nARG ALPINE_VERSION=3.19\n\nFROM docker.io/library/alpine:${ALPINE_VERSION} AS builder\nARG TARGETARCH\nARG TAR"
},
{
"path": "Dockerfile.alpine",
"chars": 585,
"preview": "ARG ALPINE_VERSION=3.19\n\nFROM docker.io/library/alpine:${ALPINE_VERSION}\nARG TARGETARCH\nARG TARGETOS\nARG TARGETVARIANT\nC"
},
{
"path": "Dockerfile.compose",
"chars": 367,
"preview": "# This Dockerfile is intented for Development purposes only to use\n# with the provided docker-compose.yaml file.\n# Pleas"
},
{
"path": "Dockerfile.e2e",
"chars": 321,
"preview": "ARG ALPINE_VERSION\nARG GOLANG_VERSION\n\nFROM docker.io/golang:${GOLANG_VERSION}-alpine${ALPINE_VERSION}\n\nWORKDIR /src/shi"
},
{
"path": "LICENSE",
"chars": 1080,
"preview": "MIT License\n\nCopyright (c) 2018-present Radhi Fadlillah\n\nPermission is hereby granted, free of charge, to any person obt"
},
{
"path": "Makefile",
"chars": 4375,
"preview": "GO ?= $(shell command -v go 2> /dev/null)\nBASH ?= $(shell command -v bash 2> /dev/null)\nGOLANG_VERSION := $(shell head -"
},
{
"path": "Procfile",
"chars": 32,
"preview": "web: bin/shiori server -p $PORT\n"
},
{
"path": "README.md",
"chars": 2622,
"preview": "# Shiori\n\n[](https://github"
},
{
"path": "app.json",
"chars": 306,
"preview": "{\n \"name\": \"Shiori\",\n \"description\": \"Shiori is a simple bookmarks manager written in Go language. Intended as a simpl"
},
{
"path": "codecov.yml",
"chars": 36,
"preview": "github_checks:\n annotations: false\n"
},
{
"path": "docker-compose.yaml",
"chars": 1484,
"preview": "# Docker compose for development purposes only.\n# Edit it to fit your current development needs.\nservices:\n shiori:\n "
},
{
"path": "docs/API.md",
"chars": 5876,
"preview": "This is a brief explanation of Shiori's API. For more examples you can import this [collection](https://github.com/go-sh"
},
{
"path": "docs/APIv1.md",
"chars": 1215,
"preview": "# API v1\n\n> ℹ️ **This is the documentation for the new API. This API is still in development and though the finished end"
},
{
"path": "docs/CLI.md",
"chars": 1182,
"preview": "Content\n---\n\n<!-- TOC -->\n\n- [Add bookmark](#add-bookmark)\n\n<!-- /TOC -->\n\nAdd bookmark\n---\n\nTo add bookmark with CLI yo"
},
{
"path": "docs/Configuration.md",
"chars": 6723,
"preview": "# Configuration\n\n<!-- TOC -->\n\n- [Overall Configuration](#overall-configuration)\n - [Global configuration](#global-conf"
},
{
"path": "docs/Contribute.md",
"chars": 4300,
"preview": "# Contribute\n\n1. [Running the server locally](#running-the-server-locally)\n2. [Updating the API documentation](#updating"
},
{
"path": "docs/Installation.md",
"chars": 5098,
"preview": "There are several installation methods available :\n\n<!-- TOC -->\n\n- [Supported](#supported)\n - [Using Precompiled Binar"
},
{
"path": "docs/Screenshots.md",
"chars": 2234,
"preview": "# Desktop Screenshots\n\n## Login Screen\n\n=== \"Light Theme\"\n []"
},
{
"path": "docs/Storage.md",
"chars": 767,
"preview": "# Storage\n\nShiori requires a folder to store several pieces of data, such as the bookmark archives, thumbnails, ebooks, "
},
{
"path": "docs/Usage.md",
"chars": 8179,
"preview": "Before using `shiori`, make sure it has been installed on your system. By default, `shiori` will store its data in direc"
},
{
"path": "docs/assets/css/style.css",
"chars": 81,
"preview": "[data-md-color-scheme=\"shiori\"] {\n --md-primary-fg-color: rgb(244, 67, 54);\n}\n"
},
{
"path": "docs/faq.md",
"chars": 9979,
"preview": "# Frequently asked questions\n\n<!-- TOC -->\n\n- [General](#general)\n - [What is this project ?](#what-is-this-project-)\n "
},
{
"path": "docs/index.md",
"chars": 449,
"preview": "# Documentation\n\nShiori is a simple bookmarks manager written in Go language. Intended as a simple clone of [Pocket](htt"
},
{
"path": "docs/postman/shiori.postman_collection.json",
"chars": 10207,
"preview": "{\n\t\"info\": {\n\t\t\"_postman_id\": \"aeadb2db-90b7-40f3-87d2-de76f8e8972a\",\n\t\t\"name\": \"shiori\",\n\t\t\"schema\": \"https://schema.ge"
},
{
"path": "docs/swagger/docs.go",
"chars": 33321,
"preview": "// Package swagger Code generated by swaggo/swag. DO NOT EDIT\npackage swagger\n\nimport \"github.com/swaggo/swag\"\n\nconst do"
},
{
"path": "docs/swagger/swagger.json",
"chars": 32541,
"preview": "{\n \"swagger\": \"2.0\",\n \"info\": {\n \"contact\": {}\n },\n \"paths\": {\n \"/api/v1/accounts\": {\n "
},
{
"path": "docs/swagger/swagger.yaml",
"chars": 15348,
"preview": "definitions:\n api_v1.bookmarkTagPayload:\n properties:\n tag_id:\n type: integer\n required:\n - tag_id"
},
{
"path": "e2e/e2eutil/containers.go",
"chars": 3966,
"preview": "package e2eutil\n\nimport (\n\t\"context\"\n\t\"io\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/require\"\n\t"
},
{
"path": "e2e/playwright/accounts_test.go",
"chars": 16916,
"preview": "package playwright\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-shiori/shiori/e2e/e2eutil\"\n\t\"github.com/playwrig"
},
{
"path": "e2e/playwright/auth_test.go",
"chars": 4756,
"preview": "package playwright\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n\n\t\"github.com/go-shiori/shiori/e2e/e2eutil\"\n\t\"github.com/playwright-commu"
},
{
"path": "e2e/playwright/playwright_test.go",
"chars": 114,
"preview": "package playwright\n\nimport \"github.com/playwright-community/playwright-go\"\n\nfunc init() {\n\tplaywright.Install()\n}\n"
},
{
"path": "e2e/playwright/reporter.go",
"chars": 3902,
"preview": "package playwright\n\nimport (\n\t\"encoding/base64\"\n\t\"fmt\"\n\t\"html/template\"\n\t\"io\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"time\"\n"
},
{
"path": "e2e/playwright/testhelper.go",
"chars": 6332,
"preview": "package playwright\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/playwrigh"
},
{
"path": "e2e/server/auth_test.go",
"chars": 868,
"preview": "package e2e\n\nimport (\n\t\"bytes\"\n\t\"net/http\"\n\t\"testing\"\n\n\t\"github.com/go-shiori/shiori/e2e/e2eutil\"\n\t\"github.com/stretchr/"
},
{
"path": "e2e/server/basic_test.go",
"chars": 435,
"preview": "package e2e\n\nimport (\n\t\"net/http\"\n\t\"testing\"\n\n\t\"github.com/go-shiori/shiori/e2e/e2eutil\"\n\t\"github.com/stretchr/testify/r"
},
{
"path": "go.mod",
"chars": 6745,
"preview": "module github.com/go-shiori/shiori\n\n// +heroku goVersion go1.25.1\ngo 1.25.1\n\nrequire (\n\tgit.sr.ht/~emersion/go-sqlite3-f"
},
{
"path": "go.sum",
"chars": 41980,
"preview": "dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=\ndario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMI"
},
{
"path": "internal/assets.go",
"chars": 70,
"preview": "package internal\n\nimport \"embed\"\n\n//go:embed view\nvar Assets embed.FS\n"
},
{
"path": "internal/cmd/add.go",
"chars": 3178,
"preview": "package cmd\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\n\t\"github.com/go-shiori/shiori/internal/core\"\n\t\"github.com/go-shiori/shior"
},
{
"path": "internal/cmd/check.go",
"chars": 3301,
"preview": "package cmd\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\t\"os\"\n\t\"sort\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/go-shiori/shiori/internal/model\"\n\t\""
},
{
"path": "internal/cmd/delete.go",
"chars": 2235,
"preview": "package cmd\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\tfp \"path/filepath\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/spf13/cobra\"\n)\n\nfunc deleteCm"
},
{
"path": "internal/cmd/export.go",
"chars": 2325,
"preview": "package cmd\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\tfp \"path/filepath\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/go-shiori/shiori/internal/model\""
},
{
"path": "internal/cmd/import.go",
"chars": 3821,
"preview": "package cmd\n\nimport (\n\t\"database/sql\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/PuerkitoBio/goq"
},
{
"path": "internal/cmd/open.go",
"chars": 4811,
"preview": "package cmd\n\nimport (\n\t\"fmt\"\n\t\"net\"\n\t\"net/http\"\n\t\"os\"\n\tfp \"path/filepath\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/go"
},
{
"path": "internal/cmd/pocket.go",
"chars": 5502,
"preview": "package cmd\n\nimport (\n\t\"context\"\n\t\"encoding/csv\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"slices\"\n\t\"strconv\"\n"
},
{
"path": "internal/cmd/pocket_test.go",
"chars": 1813,
"preview": "package cmd\n\nimport (\n\t\"context\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/go-shiori/shiori/internal/database\"\n)\n\n"
},
{
"path": "internal/cmd/print.go",
"chars": 2733,
"preview": "package cmd\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"os\"\n\n\t\"github.com/go-shiori/shiori/internal/model\"\n\t\"github.com/spf13/co"
},
{
"path": "internal/cmd/root.go",
"chars": 5829,
"preview": "package cmd\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\tfp \"path/filepath\"\n\t\"time\"\n\n\t\"github.com/go-shiori/shiori/internal/config\"\n\t\"github."
},
{
"path": "internal/cmd/serve.go",
"chars": 818,
"preview": "package cmd\n\nimport (\n\t\"github.com/spf13/cobra\"\n)\n\nfunc serveCmd() *cobra.Command {\n\tcmd := &cobra.Command{\n\t\tUse: \"se"
},
{
"path": "internal/cmd/server.go",
"chars": 3325,
"preview": "package cmd\n\nimport (\n\t\"context\"\n\t\"strings\"\n\n\t\"github.com/go-shiori/shiori/internal/config\"\n\t\"github.com/go-shiori/shior"
},
{
"path": "internal/cmd/server_test.go",
"chars": 1517,
"preview": "package cmd\n\nimport (\n\t\"testing\"\n\n\t\"github.com/go-shiori/shiori/internal/config\"\n\t\"github.com/spf13/pflag\"\n\t\"github.com/"
},
{
"path": "internal/cmd/update.go",
"chars": 8412,
"preview": "package cmd\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"sort\"\n\t\"strings\"\n\t\"sync\"\n\n\t\"github.com/go-shiori/shiori/internal/core\"\n\t\"github.com"
},
{
"path": "internal/cmd/utils.go",
"chars": 3818,
"preview": "package cmd\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\tnurl \"net/url\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path\"\n\t\"runtime\"\n\t\"strconv\"\n\t\"strings\"\n\t\"unico"
},
{
"path": "internal/cmd/utils_test.go",
"chars": 3268,
"preview": "package cmd\n\nimport (\n\t\"reflect\"\n\t\"testing\"\n)\n\nfunc Test_normalizeSpace(t *testing.T) {\n\ttests := []struct {\n\t\tname stri"
},
{
"path": "internal/cmd/version.go",
"chars": 515,
"preview": "package cmd\n\nimport (\n\t\"github.com/go-shiori/shiori/internal/model\"\n\t\"github.com/spf13/cobra\"\n)\n\nfunc newVersionCommand("
},
{
"path": "internal/config/config.go",
"chars": 6665,
"preview": "package config\n\nimport (\n\t\"bufio\"\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/gofrs/uuid/v"
},
{
"path": "internal/config/config_test.go",
"chars": 3199,
"preview": "package config\n\nimport (\n\t\"context\"\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/sirupsen/logrus\"\n\t\"github.com/stretchr/testify/requir"
},
{
"path": "internal/config/storage.go",
"chars": 634,
"preview": "package config\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\n\tgap \"github.com/muesli/go-app-paths\"\n)\n\nfunc getStorageDirector"
},
{
"path": "internal/core/core.go",
"chars": 186,
"preview": "package core\n\nimport \"github.com/go-shiori/shiori/internal/model\"\n\nvar userAgent = \"Shiori/\" + model.BuildVersion + \" (\""
},
{
"path": "internal/core/download.go",
"chars": 661,
"preview": "package core\n\nimport (\n\t\"io\"\n\t\"net/http\"\n\t\"time\"\n)\n\nvar httpClient = &http.Client{Timeout: time.Minute}\n\n// DownloadBook"
},
{
"path": "internal/core/ebook.go",
"chars": 2976,
"preview": "package core\n\nimport (\n\t\"os\"\n\tfp \"path/filepath\"\n\t\"strconv\"\n\t\"strings\"\n\n\tepub \"github.com/go-shiori/go-epub\"\n\t\"github.co"
},
{
"path": "internal/core/ebook_test.go",
"chars": 5292,
"preview": "package core_test\n\nimport (\n\t\"context\"\n\t\"os\"\n\tfp \"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/go-shiori/shiori/internal/core"
},
{
"path": "internal/core/processing.go",
"chars": 7160,
"preview": "package core\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"image\"\n\t\"image/color\"\n\t\"image/draw\"\n\t\"image/jpeg\"\n\t\"io\"\n\t\"log\"\n\t\"math\"\n\t\"net/ur"
},
{
"path": "internal/core/processing_test.go",
"chars": 10683,
"preview": "package core_test\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"os\"\n\tfp \"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/go-shiori/shiori/inte"
},
{
"path": "internal/core/url.go",
"chars": 1252,
"preview": "package core\n\nimport (\n\t\"fmt\"\n\tnurl \"net/url\"\n\t\"sort\"\n\t\"strings\"\n)\n\n// queryEncodeWithoutEmptyValues is a copy of `value"
},
{
"path": "internal/database/database.go",
"chars": 2629,
"preview": "package database\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"fmt\"\n\t\"log\"\n\t\"net/url\"\n\t\"strings\"\n\n\t\"github.com/go-shiori/shiori"
},
{
"path": "internal/database/database_tags.go",
"chars": 5779,
"preview": "package database\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"fmt\"\n\n\t\"github.com/go-shiori/shiori/internal/model\"\n\t\"github.com"
},
{
"path": "internal/database/database_tags_test.go",
"chars": 19387,
"preview": "package database\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\t\"github.com/go-shiori/shiori/internal/model\"\n\t\"github.com/stretchr/te"
},
{
"path": "internal/database/database_test.go",
"chars": 39667,
"preview": "package database\n\nimport (\n\t\"context\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-shiori/shiori/internal/model\"\n\t\"github.com/str"
},
{
"path": "internal/database/migrations/mysql/0000_system_create.up.sql",
"chars": 110,
"preview": "CREATE TABLE IF NOT EXISTS shiori_system(\n database_schema_version VARCHAR(12) NOT NULL DEFAULT '0.0.0'\n);\n"
},
{
"path": "internal/database/migrations/mysql/0000_system_insert.up.sql",
"chars": 68,
"preview": "INSERT INTO shiori_system(database_schema_version) VALUES('0.0.0');\n"
},
{
"path": "internal/database/migrations/mysql/0001_initial_account.up.sql",
"chars": 352,
"preview": "CREATE TABLE IF NOT EXISTS account(\n\t\tid INT(11) NOT NULL AUTO_INCREMENT,\n\t\tusername VARCHAR(250) NOT NUL"
},
{
"path": "internal/database/migrations/mysql/0002_initial_bookmark.up.sql",
"chars": 655,
"preview": "CREATE TABLE IF NOT EXISTS bookmark(\n\t\tid INT(11) NOT NULL AUTO_INCREMENT,\n\t\turl TEXT NOT NULL"
},
{
"path": "internal/database/migrations/mysql/0003_initial_tag.up.sql",
"chars": 189,
"preview": "CREATE TABLE IF NOT EXISTS tag(\n\t\tid INT(11) NOT NULL AUTO_INCREMENT,\n\t\tname VARCHAR(250) NOT NULL,\n\t\tPRIMARY KEY"
},
{
"path": "internal/database/migrations/mysql/0004_initial_bookmark_tag.up.sql",
"chars": 435,
"preview": "CREATE TABLE IF NOT EXISTS bookmark_tag(\n\t\tbookmark_id INT(11) NOT NULL,\n\t\ttag_id INT(11) NOT NULL,\n\t\tPRI"
},
{
"path": "internal/database/migrations/mysql/0005_rename_to_created_at.up.sql",
"chars": 59,
"preview": "ALTER TABLE bookmark RENAME COLUMN modified to created_at;\n"
},
{
"path": "internal/database/migrations/mysql/0006_change_created_at_settings.up.sql",
"chars": 85,
"preview": "ALTER TABLE bookmark\nMODIFY created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP;\n"
},
{
"path": "internal/database/migrations/mysql/0007_add_modified_at.up.sql",
"chars": 90,
"preview": "ALTER TABLE bookmark\nADD COLUMN modified_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP;\n"
},
{
"path": "internal/database/migrations/mysql/0008_set_modified_at_equal_created_at.up.sql",
"chars": 104,
"preview": "UPDATE bookmark\nSET modified_at = COALESCE(created_at, CURRENT_TIMESTAMP)\nWHERE created_at IS NOT NULL;\n"
},
{
"path": "internal/database/migrations/mysql/0009_index_for_created_at.up.sql",
"chars": 54,
"preview": "CREATE INDEX idx_created_at ON bookmark (created_at);\n"
},
{
"path": "internal/database/migrations/mysql/0010_index_for_modified_at.up.sql",
"chars": 56,
"preview": "CREATE INDEX idx_modified_at ON bookmark (modified_at);\n"
},
{
"path": "internal/database/migrations/postgres/0000_system.up.sql",
"chars": 172,
"preview": "CREATE TABLE IF NOT EXISTS shiori_system(\n database_schema_version TEXT NOT NULL DEFAULT '0.0.0'\n);\n\nINSERT INTO shio"
},
{
"path": "internal/database/migrations/postgres/0001_initial.up.sql",
"chars": 1452,
"preview": "CREATE TABLE IF NOT EXISTS account(\n\t\tid SERIAL,\n\t\tusername VARCHAR(250) NOT NULL,\n\t\tpassword BYTEA NOT NUL"
},
{
"path": "internal/database/migrations/postgres/0002_created_time.up.sql",
"chars": 603,
"preview": "-- Rename \"modified\" column to \"created_at\"\nALTER TABLE bookmark\nRENAME COLUMN modified to created_at;\n\n-- Add the \"modi"
},
{
"path": "internal/database/migrations/sqlite/0000_system.up.sql",
"chars": 172,
"preview": "CREATE TABLE IF NOT EXISTS shiori_system(\n database_schema_version TEXT NOT NULL DEFAULT '0.0.0'\n);\n\nINSERT INTO shio"
},
{
"path": "internal/database/migrations/sqlite/0001_initial.up.sql",
"chars": 1300,
"preview": "CREATE TABLE IF NOT EXISTS account(\n id INTEGER NOT NULL,\n username TEXT NOT NULL,\n password TEXT NOT NULL,\n "
},
{
"path": "internal/database/migrations/sqlite/0002_denormalize_content.up.sql",
"chars": 157,
"preview": "UPDATE bookmark\nSET has_content = bc.has_content FROM (SELECT docid, content <> '' AS has_content FROM bookmark_content)"
},
{
"path": "internal/database/migrations/sqlite/0003_uniq_id.up.sql",
"chars": 816,
"preview": "-- Create a temporary table\nCREATE TABLE IF NOT EXISTS bookmark_temp(\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n url "
},
{
"path": "internal/database/migrations/sqlite/0004_created_time.up.sql",
"chars": 309,
"preview": "ALTER TABLE bookmark\nRENAME COLUMN modified to created_at;\n\nALTER TABLE bookmark\nADD COLUMN modified_at TEXT NULL;\n\nUPDA"
},
{
"path": "internal/database/migrations.go",
"chars": 2858,
"preview": "// Package database implements database operations and migrations\npackage database\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n"
},
{
"path": "internal/database/mysql.go",
"chars": 32170,
"preview": "package database\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"fmt\"\n\t\"slices\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/go-shiori/shiori"
},
{
"path": "internal/database/mysql_test.go",
"chars": 1670,
"preview": "//go:build !test_sqlite_only\n// +build !test_sqlite_only\n\npackage database\n\nimport (\n\t\"context\"\n\t\"log\"\n\t\"os\"\n\t\"testing\"\n"
},
{
"path": "internal/database/pg.go",
"chars": 30875,
"preview": "package database\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"fmt\"\n\t\"slices\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/go-sh"
},
{
"path": "internal/database/pg_test.go",
"chars": 866,
"preview": "//go:build !test_sqlite_only\n// +build !test_sqlite_only\n\npackage database\n\nimport (\n\t\"context\"\n\t\"log\"\n\t\"os\"\n\t\"testing\"\n"
},
{
"path": "internal/database/sqlite.go",
"chars": 38434,
"preview": "package database\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"fmt\"\n\t\"log\"\n\t\"runtime\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/go-shior"
},
{
"path": "internal/database/sqlite_noncgo.go",
"chars": 981,
"preview": "//go:build linux || windows || darwin || freebsd\n// +build linux windows darwin freebsd\n\npackage database\n\nimport (\n\t\"co"
},
{
"path": "internal/database/sqlite_openbsd.go",
"chars": 980,
"preview": "//go:build openbsd\n// +build openbsd\n\npackage database\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/huandu/go-sqlbuilder\"\n\t"
},
{
"path": "internal/database/sqlite_test.go",
"chars": 2106,
"preview": "package database\n\nimport (\n\t\"context\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/go-shiori/shiori/internal/model\"\n\t"
},
{
"path": "internal/dependencies/dependencies.go",
"chars": 2042,
"preview": "package dependencies\n\nimport (\n\t\"github.com/go-shiori/shiori/internal/config\"\n\t\"github.com/go-shiori/shiori/internal/mod"
},
{
"path": "internal/domains/accounts.go",
"chars": 4034,
"preview": "package domains\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\n\t\"github.com/go-shiori/shiori/internal/database\"\n\t\"github.com/go-"
},
{
"path": "internal/domains/accounts_test.go",
"chars": 6235,
"preview": "package domains_test\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-shiori/shiori/internal/model\"\n\t\"git"
},
{
"path": "internal/domains/archiver.go",
"chars": 1429,
"preview": "package domains\n\nimport (\n\t\"fmt\"\n\t\"path/filepath\"\n\n\t\"github.com/go-shiori/shiori/internal/core\"\n\t\"github.com/go-shiori/s"
},
{
"path": "internal/domains/auth.go",
"chars": 2340,
"preview": "package domains\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/go-shiori/shiori/internal/dependencies\"\n\t\"github.com/g"
},
{
"path": "internal/domains/auth_test.go",
"chars": 3316,
"preview": "package domains_test\n\nimport (\n\t\"context\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-shiori/shiori/internal/domains\"\n\t\"github.c"
},
{
"path": "internal/domains/bookmark_tags_test.go",
"chars": 4282,
"preview": "package domains_test\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\t\"github.com/go-shiori/shiori/internal/model\"\n\t\"github.com/go-shio"
},
{
"path": "internal/domains/bookmarks.go",
"chars": 4706,
"preview": "package domains\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/go-shiori/shiori/internal/core\"\n\t\"github.com/go-shiori/shiori/"
},
{
"path": "internal/domains/bookmarks_test.go",
"chars": 8964,
"preview": "package domains_test\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"testing\"\n\n\t\"github.com/go-shiori/shiori/internal/domains\"\n\t\"github"
},
{
"path": "internal/domains/storage.go",
"chars": 2200,
"preview": "package domains\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"io/fs\"\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"github.com/go-shiori/shiori/internal/model\"\n\t\""
},
{
"path": "internal/domains/storage_test.go",
"chars": 2384,
"preview": "package domains_test\n\nimport (\n\t\"context\"\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/go-shiori/shiori/internal/domains\"\n\t\"github.com"
},
{
"path": "internal/domains/tags.go",
"chars": 1944,
"preview": "package domains\n\nimport (\n\t\"context\"\n\t\"errors\"\n\n\t\"github.com/go-shiori/shiori/internal/database\"\n\t\"github.com/go-shiori/"
},
{
"path": "internal/domains/tags_test.go",
"chars": 7989,
"preview": "package domains_test\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/go-shiori/shiori/internal/model\""
},
{
"path": "internal/http/handlers/api/v1/accounts.go",
"chars": 5140,
"preview": "package api_v1\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"net/http\"\n\t\"strconv\"\n\n\t\"github.com/go-shiori/shiori/internal/http/"
},
{
"path": "internal/http/handlers/api/v1/accounts_test.go",
"chars": 16283,
"preview": "package api_v1\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"testing\"\n\n\t\"github.com/go-shiori/shiori/internal/model\"\n\t\"g"
},
{
"path": "internal/http/handlers/api/v1/auth.go",
"chars": 6336,
"preview": "package api_v1\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/go-shiori/shiori/internal/http/middle"
},
{
"path": "internal/http/handlers/api/v1/auth_test.go",
"chars": 8008,
"preview": "package api_v1\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\t\"testing\"\n\n\t\"github.com/go-shiori/shiori/internal/model\"\n\t\"github.com/g"
},
{
"path": "internal/http/handlers/api/v1/bookmark_tags_test.go",
"chars": 21191,
"preview": "package api_v1_test\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"testing\"\n\n\tapi_v1 \"github.com/go-shio"
},
{
"path": "internal/http/handlers/api/v1/bookmarks.go",
"chars": 11709,
"preview": "package api_v1\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"sync\"\n\n\t\"github.com/go-shiori/shiori"
},
{
"path": "internal/http/handlers/api/v1/bookmarks_test.go",
"chars": 8887,
"preview": "package api_v1\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"io\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-sh"
},
{
"path": "internal/http/handlers/api/v1/system.go",
"chars": 1333,
"preview": "package api_v1\n\nimport (\n\t\"net/http\"\n\t\"runtime\"\n\n\t\"github.com/go-shiori/shiori/internal/http/middleware\"\n\t\"github.com/go"
},
{
"path": "internal/http/handlers/api/v1/system_test.go",
"chars": 1421,
"preview": "package api_v1\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\t\"testing\"\n\n\t\"github.com/go-shiori/shiori/internal/testutil\"\n\t\"github.co"
},
{
"path": "internal/http/handlers/api/v1/tags.go",
"chars": 6786,
"preview": "package api_v1\n\nimport (\n\t\"encoding/json\"\n\t\"net/http\"\n\t\"strconv\"\n\n\t\"github.com/go-shiori/shiori/internal/http/middleware"
},
{
"path": "internal/http/handlers/api/v1/tags_test.go",
"chars": 15897,
"preview": "package api_v1\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/go-shiori/shiori/internal"
},
{
"path": "internal/http/handlers/api.go",
"chars": 813,
"preview": "package handlers\n\nimport (\n\t\"net/http\"\n\n\t\"github.com/go-shiori/shiori/internal/dependencies\"\n\t\"github.com/sirupsen/logru"
},
{
"path": "internal/http/handlers/bookmark.go",
"chars": 5061,
"preview": "package handlers\n\nimport (\n\t\"fmt\"\n\t\"html/template\"\n\t\"net/http\"\n\t\"strconv\"\n\n\t\"github.com/go-shiori/shiori/internal/http/r"
},
{
"path": "internal/http/handlers/bookmark_test.go",
"chars": 6947,
"preview": "package handlers\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"testing\"\n\n\t\"github.com/go-shiori/shiori/internal/http/tem"
},
{
"path": "internal/http/handlers/frontend.go",
"chars": 1347,
"preview": "package handlers\n\nimport (\n\t\"embed\"\n\t\"net/http\"\n\t\"path\"\n\n\t\"github.com/go-shiori/shiori/internal/http/response\"\n\t\"github."
},
{
"path": "internal/http/handlers/frontend_test.go",
"chars": 1330,
"preview": "package handlers\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\t\"testing\"\n\n\t\"github.com/go-shiori/shiori/internal/http/templates\"\n\t\"g"
},
{
"path": "internal/http/handlers/legacy.go",
"chars": 3866,
"preview": "package handlers\n\nimport (\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/go-shiori/shiori/internal/model\"\n\t\"github.com/go-shiori/shi"
},
{
"path": "internal/http/handlers/legacy_test.go",
"chars": 2284,
"preview": "package handlers\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-shiori/shiori/internal/mode"
},
{
"path": "internal/http/handlers/swagger.go",
"chars": 766,
"preview": "package handlers\n\nimport (\n\t\"net/http\"\n\n\t\"github.com/go-shiori/shiori/internal/model\"\n\thttpSwagger \"github.com/swaggo/ht"
},
{
"path": "internal/http/handlers/swagger_test.go",
"chars": 1081,
"preview": "package handlers\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\t\"testing\"\n\n\t\"github.com/go-shiori/shiori/internal/testutil\"\n\t\"github."
},
{
"path": "internal/http/handlers/system.go",
"chars": 507,
"preview": "package handlers\n\nimport (\n\t\"net/http\"\n\n\t\"github.com/go-shiori/shiori/internal/http/response\"\n\t\"github.com/go-shiori/shi"
},
{
"path": "internal/http/handlers/system_test.go",
"chars": 839,
"preview": "package handlers\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\t\"github.com/go-shiori/shiori/internal/testutil\"\n\t\"github.com/sirupsen"
},
{
"path": "internal/http/http.go",
"chars": 1073,
"preview": "package http\n\nimport (\n\t\"net/http\"\n\n\t\"github.com/go-shiori/shiori/internal/http/webcontext\"\n\t\"github.com/go-shiori/shior"
},
{
"path": "internal/http/http_test.go",
"chars": 2733,
"preview": "package http\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"net/http\"\n\t\"testing\"\n\n\t\"github.com/go-shiori/shiori/internal/model\"\n\t\"gith"
},
{
"path": "internal/http/middleware/auth.go",
"chars": 2620,
"preview": "package middleware\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\t\"strings\"\n\n\t\"github.com/go-shiori/shiori/internal/http/response\"\n\t\"gith"
},
{
"path": "internal/http/middleware/auth_sso_proxy.go",
"chars": 2474,
"preview": "package middleware\n\nimport (\n\t\"errors\"\n\t\"net\"\n\n\t\"github.com/go-shiori/shiori/internal/model\"\n)\n\n// AuthMiddleware handle"
},
{
"path": "internal/http/middleware/auth_sso_proxy_test.go",
"chars": 2965,
"preview": "package middleware\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/go-shiori/shiori/inter"
},
{
"path": "internal/http/middleware/auth_test.go",
"chars": 4735,
"preview": "package middleware\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-shiori/shio"
},
{
"path": "internal/http/middleware/cors.go",
"chars": 1126,
"preview": "package middleware\n\nimport (\n\t\"strings\"\n\n\t\"github.com/go-shiori/shiori/internal/model\"\n)\n\ntype CORSMiddleware struct {\n\t"
},
{
"path": "internal/http/middleware/cors_test.go",
"chars": 2493,
"preview": "package middleware\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/go-shiori/shiori/inter"
},
{
"path": "internal/http/middleware/logging.go",
"chars": 821,
"preview": "package middleware\n\nimport (\n\t\"time\"\n\n\t\"github.com/go-shiori/shiori/internal/model\"\n\t\"github.com/sirupsen/logrus\"\n)\n\nvar"
},
{
"path": "internal/http/middleware/message_response.go",
"chars": 2993,
"preview": "package middleware\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"net/http\"\n\t\"strings\"\n\n\t\"github.com/go-shiori/shiori/internal/mo"
},
{
"path": "internal/http/middleware/message_response_test.go",
"chars": 5415,
"preview": "package middleware\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"net/http\"\n\t\"testing\"\n\n\t\"github.com/go-shiori/shiori/internal/"
},
{
"path": "internal/http/middleware/request_id.go",
"chars": 1187,
"preview": "package middleware\n\nimport (\n\t\"github.com/go-shiori/shiori/internal/model\"\n\t\"github.com/gofrs/uuid/v5\"\n)\n\nconst (\n\t// Re"
},
{
"path": "internal/http/middleware/request_id_test.go",
"chars": 835,
"preview": "package middleware\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\t\"github.com/go-shiori/shiori/internal/testutil\"\n\t\"github.com/sirups"
},
{
"path": "internal/http/response/file.go",
"chars": 1732,
"preview": "package response\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"mime\"\n\t\"net/http\"\n\t\"path/filepath\"\n\n\t\"github.com/go-shiori/shiori/internal/mod"
},
{
"path": "internal/http/response/file_test.go",
"chars": 4227,
"preview": "package response_test\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"io\"\n\t\"net/http\"\n\t\"testing\"\n\n\t\"github.com/go-shiori/shiori/internal"
},
{
"path": "internal/http/response/response.go",
"chars": 1298,
"preview": "package response\n\nimport (\n\t\"encoding/json\"\n\t\"net/http\"\n\n\t\"github.com/go-shiori/shiori/internal/model\"\n)\n\ntype Response "
},
{
"path": "internal/http/response/response_test.go",
"chars": 3042,
"preview": "package response\n\nimport (\n\t\"encoding/json\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/go-shiori/shiori/i"
},
{
"path": "internal/http/response/shortcuts.go",
"chars": 2348,
"preview": "package response\n\nimport (\n\t\"net/http\"\n\t\"net/url\"\n\n\t\"github.com/go-shiori/shiori/internal/http/templates\"\n\t\"github.com/g"
},
{
"path": "internal/http/response/shortcuts_test.go",
"chars": 4637,
"preview": "package response_test\n\nimport (\n\t\"encoding/json\"\n\t\"net/http\"\n\t\"testing\"\n\n\t\"github.com/go-shiori/shiori/internal/http/res"
},
{
"path": "internal/http/server.go",
"chars": 7703,
"preview": "package http\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"os\"\n\t\"os/signal\"\n\t\"syscall\"\n\n\t\"github.com/go-shiori/shiori/intern"
},
{
"path": "internal/http/server_test.go",
"chars": 6639,
"preview": "package http\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github."
},
{
"path": "internal/http/templates/templates.go",
"chars": 1031,
"preview": "package templates\n\nimport (\n\t\"fmt\"\n\t\"html/template\"\n\t\"io\"\n\n\t\"github.com/go-shiori/shiori/internal/config\"\n\tviews \"github"
},
{
"path": "internal/http/webcontext/auth.go",
"chars": 877,
"preview": "package webcontext\n\nimport (\n\t\"context\"\n\n\t\"github.com/go-shiori/shiori/internal/model\"\n)\n\n// UserIsLogged returns a bool"
},
{
"path": "internal/http/webcontext/auth_test.go",
"chars": 1714,
"preview": "package webcontext\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/go-shiori/shiori/internal/model\"\n"
},
{
"path": "internal/http/webcontext/context.go",
"chars": 1506,
"preview": "package webcontext\n\nimport (\n\t\"context\"\n\t\"net/http\"\n)\n\n// WebContext wraps the standard request and response writer\ntype"
},
{
"path": "internal/http/webcontext/keys.go",
"chars": 130,
"preview": "package webcontext\n\ntype contextKey string\n\nconst (\n\taccountKey contextKey = \"account\"\n\trequestIDKey contextKey = \"req"
},
{
"path": "internal/model/account.go",
"chars": 2189,
"preview": "package model\n\nimport (\n\t\"database/sql/driver\"\n\t\"encoding/json\"\n\t\"fmt\"\n\n\t\"github.com/golang-jwt/jwt/v5\"\n)\n\n// Account is"
},
{
"path": "internal/model/bookmark.go",
"chars": 2996,
"preview": "package model\n\nimport (\n\t\"path/filepath\"\n\t\"strconv\"\n)\n\n// Bookmark is the database representation of a bookmark\ntype Boo"
},
{
"path": "internal/model/bookmark_test.go",
"chars": 6382,
"preview": "package model\n\nimport (\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestBookmarkToDTO(t *"
},
{
"path": "internal/model/const.go",
"chars": 246,
"preview": "package model\n\n// DataDirPerm the default filesystem permissions for the data directory/archives\nconst DataDirPerm = 074"
},
{
"path": "internal/model/database.go",
"chars": 4947,
"preview": "package model\n\nimport (\n\t\"context\"\n\n\t\"github.com/jmoiron/sqlx\"\n)\n\ntype DBID int\n\n// DB is interface for accessing and ma"
},
{
"path": "internal/model/dependencies.go",
"chars": 770,
"preview": "package model\n\nimport (\n\t\"github.com/go-shiori/shiori/internal/config\"\n\t\"github.com/sirupsen/logrus\"\n)\n\n// Dependencies "
},
{
"path": "internal/model/domains.go",
"chars": 2224,
"preview": "package model\n\nimport (\n\t\"context\"\n\t\"io/fs\"\n\t\"os\"\n\t\"time\"\n\n\t\"github.com/go-shiori/warc\"\n\t\"github.com/spf13/afero\"\n)\n\ntyp"
},
{
"path": "internal/model/errors.go",
"chars": 353,
"preview": "package model\n\nimport \"errors\"\n\nvar (\n\tErrBookmarkNotFound = errors.New(\"bookmark not found\")\n\tErrBookmarkInvalidID = e"
},
{
"path": "internal/model/http.go",
"chars": 1049,
"preview": "package model\n\nimport \"net/http\"\n\nconst (\n\t// ContextAccountKey is the key used to store the account model in the gin co"
},
{
"path": "internal/model/legacy.go",
"chars": 119,
"preview": "package model\n\nimport \"time\"\n\ntype LegacyLoginHandler func(account *AccountDTO, expTime time.Duration) (string, error)\n"
},
{
"path": "internal/model/main.go",
"chars": 242,
"preview": "package model\n\n// Variables set my the main package coming from ldflags\nvar (\n\tBuildVersion = \"dev\"\n\tBuildCommit = \"non"
},
{
"path": "internal/model/ptr.go",
"chars": 112,
"preview": "package model\n\n// Ptr returns a pointer to the value passed as argument.\nfunc Ptr[t any](a t) *t {\n\treturn &a\n}\n"
},
{
"path": "internal/model/slices.go",
"chars": 566,
"preview": "package model\n\n// SliceDifference returns the elements that are in haystack but not in needle.\n// It's a generic functio"
},
{
"path": "internal/model/slices_test.go",
"chars": 1684,
"preview": "package model\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestSliceDifference(t *testing.T) {\n\tt"
}
]
// ... and 128 more files (download for full content)
About this extraction
This page contains the full source code of the go-shiori/shiori GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 328 files (1.7 MB), approximately 514.9k tokens, and a symbol index with 2014 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.