Merge branch 'master' of https://github.com/rr-/szurubooru
This commit is contained in:
commit
ed544b52a8
|
@ -0,0 +1,5 @@
|
|||
# Auto detect text files and perform LF normalization
|
||||
* text=auto
|
||||
|
||||
# Shell scripts require LF
|
||||
*.sh text eol=lf
|
|
@ -1,5 +1,8 @@
|
|||
name: Build Docker containers
|
||||
on: [push]
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
jobs:
|
||||
build-client:
|
||||
name: Build and push client/ Docker container
|
||||
|
|
|
@ -1,28 +1,29 @@
|
|||
repos:
|
||||
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v3.2.0
|
||||
rev: v4.0.1
|
||||
hooks:
|
||||
- id: trailing-whitespace
|
||||
- id: end-of-file-fixer
|
||||
- id: check-yaml
|
||||
- id: mixed-line-ending
|
||||
|
||||
|
||||
- repo: https://github.com/Lucas-C/pre-commit-hooks
|
||||
rev: v1.1.9
|
||||
rev: v1.1.10
|
||||
hooks:
|
||||
- id: remove-tabs
|
||||
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 20.8b1
|
||||
rev: '22.3.0'
|
||||
hooks:
|
||||
- id: black
|
||||
files: 'server/'
|
||||
types: [python]
|
||||
language_version: python3.8
|
||||
language_version: python3.9
|
||||
|
||||
- repo: https://github.com/timothycrosley/isort
|
||||
rev: '5.4.2'
|
||||
- repo: https://github.com/PyCQA/isort
|
||||
rev: '5.10.1'
|
||||
hooks:
|
||||
- id: isort
|
||||
files: 'server/'
|
||||
|
@ -31,8 +32,8 @@ repos:
|
|||
additional_dependencies:
|
||||
- toml
|
||||
|
||||
- repo: https://github.com/prettier/prettier
|
||||
rev: '2.1.1'
|
||||
- repo: https://github.com/pre-commit/mirrors-prettier
|
||||
rev: v2.5.0
|
||||
hooks:
|
||||
- id: prettier
|
||||
files: client/js/
|
||||
|
@ -40,7 +41,7 @@ repos:
|
|||
args: ['--config', 'client/.prettierrc.yml']
|
||||
|
||||
- repo: https://github.com/pre-commit/mirrors-eslint
|
||||
rev: v7.8.0
|
||||
rev: v8.3.0
|
||||
hooks:
|
||||
- id: eslint
|
||||
files: client/js/
|
||||
|
@ -48,8 +49,8 @@ repos:
|
|||
additional_dependencies:
|
||||
- eslint-config-prettier
|
||||
|
||||
- repo: https://gitlab.com/pycqa/flake8
|
||||
rev: '3.8.3'
|
||||
- repo: https://gitlab.com/PyCQA/flake8
|
||||
rev: '4.0.1'
|
||||
hooks:
|
||||
- id: flake8
|
||||
files: server/szurubooru/
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
Szurubooru is an image board engine inspired by services such as Danbooru,
|
||||
Gelbooru and Moebooru dedicated for small and medium communities. Its name [has
|
||||
its roots in Polish language and has onomatopeic meaning of scraping or
|
||||
scrubbing](http://sjp.pwn.pl/sjp/;2527372). It is pronounced as *shoorubooru*.
|
||||
scrubbing](https://sjp.pwn.pl/sjp/;2527372). It is pronounced as *shoorubooru*.
|
||||
|
||||
## Features
|
||||
|
||||
|
|
|
@ -2,7 +2,6 @@ FROM node:lts as builder
|
|||
WORKDIR /opt/app
|
||||
|
||||
COPY package.json package-lock.json ./
|
||||
RUN npm install -g npm
|
||||
RUN npm install
|
||||
|
||||
COPY . ./
|
||||
|
|
|
@ -14,9 +14,11 @@ $cancel-button-color = tomato
|
|||
&.inactive input[type=submit],
|
||||
&.inactive .skip-duplicates
|
||||
&.inactive .always-upload-similar
|
||||
&.inactive .pause-remain-on-error
|
||||
&.uploading input[type=submit],
|
||||
&.uploading .skip-duplicates,
|
||||
&.uploading .always-upload-similar
|
||||
&.uploading .pause-remain-on-error
|
||||
&:not(.uploading) .cancel
|
||||
display: none
|
||||
|
||||
|
@ -44,6 +46,9 @@ $cancel-button-color = tomato
|
|||
.always-upload-similar
|
||||
margin-left: 1em
|
||||
|
||||
.pause-remain-on-error
|
||||
margin-left: 1em
|
||||
|
||||
form>.messages
|
||||
margin-top: 1em
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
<span class='skip-duplicates'>
|
||||
<%= ctx.makeCheckbox({
|
||||
text: 'Skip duplicates',
|
||||
text: 'Skip duplicate',
|
||||
name: 'skip-duplicates',
|
||||
checked: false,
|
||||
}) %>
|
||||
|
@ -15,12 +15,20 @@
|
|||
|
||||
<span class='always-upload-similar'>
|
||||
<%= ctx.makeCheckbox({
|
||||
text: 'Always upload similar',
|
||||
text: 'Force upload similar',
|
||||
name: 'always-upload-similar',
|
||||
checked: false,
|
||||
}) %>
|
||||
</span>
|
||||
|
||||
<span class='pause-remain-on-error'>
|
||||
<%= ctx.makeCheckbox({
|
||||
text: 'Pause on error',
|
||||
name: 'pause-remain-on-error',
|
||||
checked: true,
|
||||
}) %>
|
||||
</span>
|
||||
|
||||
<input type='button' value='Cancel' class='cancel'/>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -90,21 +90,30 @@ class PostUploadController {
|
|||
uploadable
|
||||
);
|
||||
}
|
||||
if (e.detail.pauseRemainOnError) {
|
||||
return Promise.reject();
|
||||
}
|
||||
})
|
||||
),
|
||||
Promise.resolve()
|
||||
)
|
||||
.then(() => {
|
||||
if (anyFailures) {
|
||||
this._view.showError(genericErrorMessage);
|
||||
this._view.enableForm();
|
||||
} else {
|
||||
return Promise.reject();
|
||||
}
|
||||
})
|
||||
.then(
|
||||
() => {
|
||||
this._view.clearMessages();
|
||||
misc.disableExitConfirmation();
|
||||
const ctx = router.show(uri.formatClientLink("posts"));
|
||||
ctx.controller.showSuccess("Posts uploaded.");
|
||||
},
|
||||
(error) => {
|
||||
this._view.showError(genericErrorMessage);
|
||||
this._view.enableForm();
|
||||
}
|
||||
});
|
||||
);
|
||||
}
|
||||
|
||||
_uploadSinglePost(uploadable, skipDuplicates, alwaysUploadSimilar) {
|
||||
|
|
|
@ -31,9 +31,8 @@ class UserController {
|
|||
userTokenPromise = UserToken.get(userName).then(
|
||||
(userTokens) => {
|
||||
return userTokens.map((token) => {
|
||||
token.isCurrentAuthToken = api.isCurrentAuthToken(
|
||||
token
|
||||
);
|
||||
token.isCurrentAuthToken =
|
||||
api.isCurrentAuthToken(token);
|
||||
return token;
|
||||
});
|
||||
},
|
||||
|
|
|
@ -45,9 +45,8 @@ class ExpanderControl {
|
|||
// eslint-disable-next-line accessor-pairs
|
||||
set title(newTitle) {
|
||||
if (this._expanderNode) {
|
||||
this._expanderNode.querySelector(
|
||||
"header span"
|
||||
).textContent = newTitle;
|
||||
this._expanderNode.querySelector("header span").textContent =
|
||||
newTitle;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -203,9 +203,8 @@ class PostEditSidebarControl extends events.EventTarget {
|
|||
);
|
||||
|
||||
if (this._formNode) {
|
||||
const inputNodes = this._formNode.querySelectorAll(
|
||||
"input, textarea"
|
||||
);
|
||||
const inputNodes =
|
||||
this._formNode.querySelectorAll("input, textarea");
|
||||
for (let node of inputNodes) {
|
||||
node.addEventListener("change", (e) =>
|
||||
this.dispatchEvent(new CustomEvent("change"))
|
||||
|
|
|
@ -727,9 +727,8 @@ class PostNotesOverlayControl extends events.EventTarget {
|
|||
}
|
||||
|
||||
_showNoteText(note) {
|
||||
this._textNode.querySelector(
|
||||
".wrapper"
|
||||
).innerHTML = misc.formatMarkdown(note.text);
|
||||
this._textNode.querySelector(".wrapper").innerHTML =
|
||||
misc.formatMarkdown(note.text);
|
||||
this._textNode.style.display = "block";
|
||||
const bodyRect = document.body.getBoundingClientRect();
|
||||
const noteRect = this._textNode.getBoundingClientRect();
|
||||
|
|
|
@ -65,17 +65,6 @@ class TagPermalinkFixWrapper extends BaseMarkdownWrapper {
|
|||
// post, user and tags permalinks
|
||||
class EntityPermalinkWrapper extends BaseMarkdownWrapper {
|
||||
preprocess(text) {
|
||||
// URL-based permalinks
|
||||
text = text.replace(new RegExp("\\b/post/(\\d+)/?\\b", "g"), "@$1");
|
||||
text = text.replace(
|
||||
new RegExp("\\b/tag/([a-zA-Z0-9_-]+?)/?", "g"),
|
||||
"#$1"
|
||||
);
|
||||
text = text.replace(
|
||||
new RegExp("\\b/user/([a-zA-Z0-9_-]+?)/?", "g"),
|
||||
"+$1"
|
||||
);
|
||||
|
||||
text = text.replace(
|
||||
/(^|^\(|(?:[^\]])\(|[\s<>\[\]\)])([+#@][a-zA-Z0-9_-]+)/g,
|
||||
"$1[$2]($2)"
|
||||
|
@ -136,12 +125,8 @@ function createRenderer() {
|
|||
|
||||
const renderer = new marked.Renderer();
|
||||
renderer.image = (href, title, alt) => {
|
||||
let [
|
||||
_,
|
||||
url,
|
||||
width,
|
||||
height,
|
||||
] = /^(.+?)(?:\s=\s*(\d*)\s*x\s*(\d*)\s*)?$/.exec(href);
|
||||
let [_, url, width, height] =
|
||||
/^(.+?)(?:\s=\s*(\d*)\s*x\s*(\d*)\s*)?$/.exec(href);
|
||||
let res = '<img src="' + sanitize(url) + '" alt="' + sanitize(alt);
|
||||
if (width) {
|
||||
res += '" width="' + width;
|
||||
|
@ -174,7 +159,7 @@ function formatMarkdown(text) {
|
|||
for (let wrapper of wrappers) {
|
||||
text = wrapper.preprocess(text);
|
||||
}
|
||||
text = marked(text, options);
|
||||
text = marked.parse(text, options);
|
||||
wrappers.reverse();
|
||||
for (let wrapper of wrappers) {
|
||||
text = wrapper.postprocess(text);
|
||||
|
@ -200,7 +185,7 @@ function formatInlineMarkdown(text) {
|
|||
for (let wrapper of wrappers) {
|
||||
text = wrapper.preprocess(text);
|
||||
}
|
||||
text = marked.inlineLexer(text, [], options);
|
||||
text = marked.parseInline(text, options);
|
||||
wrappers.reverse();
|
||||
for (let wrapper of wrappers) {
|
||||
text = wrapper.postprocess(text);
|
||||
|
|
|
@ -25,9 +25,8 @@ class PostMainView {
|
|||
views.replaceContent(this._hostNode, sourceNode);
|
||||
views.syncScrollPosition();
|
||||
|
||||
const topNavigationNode = document.body.querySelector(
|
||||
"#top-navigation"
|
||||
);
|
||||
const topNavigationNode =
|
||||
document.body.querySelector("#top-navigation");
|
||||
|
||||
this._postContentControl = new PostContentControl(
|
||||
postContainerNode,
|
||||
|
|
|
@ -285,7 +285,7 @@ class PostUploadView extends events.EventTarget {
|
|||
for (let uploadable of this._uploadables) {
|
||||
this._updateUploadableFromDom(uploadable);
|
||||
}
|
||||
this._submitButtonNode.value = "Resume upload";
|
||||
this._submitButtonNode.value = "Resume";
|
||||
this._emit("submit");
|
||||
}
|
||||
|
||||
|
@ -360,8 +360,10 @@ class PostUploadView extends events.EventTarget {
|
|||
detail: {
|
||||
uploadables: this._uploadables,
|
||||
skipDuplicates: this._skipDuplicatesCheckboxNode.checked,
|
||||
alwaysUploadSimilar: this._alwaysUploadSimilarCheckboxNode
|
||||
.checked,
|
||||
alwaysUploadSimilar:
|
||||
this._alwaysUploadSimilarCheckboxNode.checked,
|
||||
pauseRemainOnError:
|
||||
this._pauseRemainOnErrorCheckboxNode.checked,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
@ -431,6 +433,12 @@ class PostUploadView extends events.EventTarget {
|
|||
);
|
||||
}
|
||||
|
||||
get _pauseRemainOnErrorCheckboxNode() {
|
||||
return this._hostNode.querySelector(
|
||||
"form [name=pause-remain-on-error]"
|
||||
);
|
||||
}
|
||||
|
||||
get _submitButtonNode() {
|
||||
return this._hostNode.querySelector("form [type=submit]");
|
||||
}
|
||||
|
|
|
@ -253,9 +253,8 @@ class PostsHeaderView extends events.EventTarget {
|
|||
e.target.classList.toggle("disabled");
|
||||
const safety = e.target.getAttribute("data-safety");
|
||||
let browsingSettings = settings.get();
|
||||
browsingSettings.listPosts[safety] = !browsingSettings.listPosts[
|
||||
safety
|
||||
];
|
||||
browsingSettings.listPosts[safety] =
|
||||
!browsingSettings.listPosts[safety];
|
||||
settings.save(browsingSettings, true);
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("navigate", {
|
||||
|
|
|
@ -72,9 +72,8 @@ class UserTokenView extends events.EventTarget {
|
|||
|
||||
_evtDelete(e) {
|
||||
e.preventDefault();
|
||||
const userToken = this._tokens[
|
||||
parseInt(e.target.getAttribute("data-token-id"))
|
||||
];
|
||||
const userToken =
|
||||
this._tokens[parseInt(e.target.getAttribute("data-token-id"))];
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("delete", {
|
||||
detail: {
|
||||
|
@ -110,9 +109,8 @@ class UserTokenView extends events.EventTarget {
|
|||
|
||||
_evtChangeNoteClick(e) {
|
||||
e.preventDefault();
|
||||
const userToken = this._tokens[
|
||||
parseInt(e.target.getAttribute("data-token-id"))
|
||||
];
|
||||
const userToken =
|
||||
this._tokens[parseInt(e.target.getAttribute("data-token-id"))];
|
||||
const text = window.prompt(
|
||||
"Please enter the new name:",
|
||||
userToken.note !== null ? userToken.note : undefined
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
"font-awesome": "^4.7.0",
|
||||
"ios-inner-height": "^1.0.3",
|
||||
"js-cookie": "^2.2.0",
|
||||
"marked": "^0.7.0",
|
||||
"marked": "^4.0.10",
|
||||
"mousetrap": "^1.6.2",
|
||||
"nprogress": "^0.2.0",
|
||||
"superagent": "^3.8.3"
|
||||
|
@ -1506,9 +1506,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"node_modules/cached-path-relative": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/cached-path-relative/-/cached-path-relative-1.0.2.tgz",
|
||||
"integrity": "sha512-5r2GqsoEb4qMTTN9J+WzXfjov+hjxT+j3u5K+kIVNIwAd99DLCJE9pBIMP1qVeybV6JiijL385Oz0DcYxfbOIg==",
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/cached-path-relative/-/cached-path-relative-1.1.0.tgz",
|
||||
"integrity": "sha512-WF0LihfemtesFcJgO7xfOoOcnWzY/QHR4qeDqV44jPU3HTI54+LnfXK3SA27AVVGCdZFgjjFFaqUA9Jx7dMJZA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/call-bind": {
|
||||
|
@ -2997,14 +2997,14 @@
|
|||
"dev": true
|
||||
},
|
||||
"node_modules/marked": {
|
||||
"version": "0.7.0",
|
||||
"resolved": "https://registry.npmjs.org/marked/-/marked-0.7.0.tgz",
|
||||
"integrity": "sha512-c+yYdCZJQrsRjTPhUx7VKkApw9bwDkNbHUKo1ovgcfDjb2kc8rLuRbIFyXL5WOEUwzSSKo3IXpph2K6DqB/KZg==",
|
||||
"version": "4.0.10",
|
||||
"resolved": "https://registry.npmjs.org/marked/-/marked-4.0.10.tgz",
|
||||
"integrity": "sha512-+QvuFj0nGgO970fySghXGmuw+Fd0gD2x3+MqCWLIPf5oxdv1Ka6b2q+z9RP01P/IaKPMEramy+7cNy/Lw8c3hw==",
|
||||
"bin": {
|
||||
"marked": "bin/marked"
|
||||
"marked": "bin/marked.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
"node": ">= 12"
|
||||
}
|
||||
},
|
||||
"node_modules/md5.js": {
|
||||
|
@ -3108,9 +3108,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/minimist": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
|
||||
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
|
||||
"version": "1.2.6",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
|
||||
"integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/mkdirp": {
|
||||
|
@ -3385,9 +3385,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/path-parse": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz",
|
||||
"integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==",
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
|
||||
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/path-platform": {
|
||||
|
@ -3983,12 +3983,6 @@
|
|||
"minimist": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/subarg/node_modules/minimist": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
|
||||
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/superagent": {
|
||||
"version": "3.8.3",
|
||||
"resolved": "https://registry.npmjs.org/superagent/-/superagent-3.8.3.tgz",
|
||||
|
@ -6053,9 +6047,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"cached-path-relative": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/cached-path-relative/-/cached-path-relative-1.0.2.tgz",
|
||||
"integrity": "sha512-5r2GqsoEb4qMTTN9J+WzXfjov+hjxT+j3u5K+kIVNIwAd99DLCJE9pBIMP1qVeybV6JiijL385Oz0DcYxfbOIg==",
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/cached-path-relative/-/cached-path-relative-1.1.0.tgz",
|
||||
"integrity": "sha512-WF0LihfemtesFcJgO7xfOoOcnWzY/QHR4qeDqV44jPU3HTI54+LnfXK3SA27AVVGCdZFgjjFFaqUA9Jx7dMJZA==",
|
||||
"dev": true
|
||||
},
|
||||
"call-bind": {
|
||||
|
@ -7280,9 +7274,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"marked": {
|
||||
"version": "0.7.0",
|
||||
"resolved": "https://registry.npmjs.org/marked/-/marked-0.7.0.tgz",
|
||||
"integrity": "sha512-c+yYdCZJQrsRjTPhUx7VKkApw9bwDkNbHUKo1ovgcfDjb2kc8rLuRbIFyXL5WOEUwzSSKo3IXpph2K6DqB/KZg=="
|
||||
"version": "4.0.10",
|
||||
"resolved": "https://registry.npmjs.org/marked/-/marked-4.0.10.tgz",
|
||||
"integrity": "sha512-+QvuFj0nGgO970fySghXGmuw+Fd0gD2x3+MqCWLIPf5oxdv1Ka6b2q+z9RP01P/IaKPMEramy+7cNy/Lw8c3hw=="
|
||||
},
|
||||
"md5.js": {
|
||||
"version": "1.3.4",
|
||||
|
@ -7364,9 +7358,9 @@
|
|||
}
|
||||
},
|
||||
"minimist": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
|
||||
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
|
||||
"version": "1.2.6",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
|
||||
"integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==",
|
||||
"dev": true
|
||||
},
|
||||
"mkdirp": {
|
||||
|
@ -7607,9 +7601,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"path-parse": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz",
|
||||
"integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==",
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
|
||||
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
|
||||
"dev": true
|
||||
},
|
||||
"path-platform": {
|
||||
|
@ -8116,14 +8110,6 @@
|
|||
"dev": true,
|
||||
"requires": {
|
||||
"minimist": "^1.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"minimist": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
|
||||
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"superagent": {
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
"font-awesome": "^4.7.0",
|
||||
"ios-inner-height": "^1.0.3",
|
||||
"js-cookie": "^2.2.0",
|
||||
"marked": "^0.7.0",
|
||||
"marked": "^4.0.10",
|
||||
"mousetrap": "^1.6.2",
|
||||
"nprogress": "^0.2.0",
|
||||
"superagent": "^3.8.3"
|
||||
|
|
24
doc/API.md
24
doc/API.md
|
@ -37,6 +37,7 @@
|
|||
- [Creating post](#creating-post)
|
||||
- [Updating post](#updating-post)
|
||||
- [Getting post](#getting-post)
|
||||
- [Getting around post](#getting-around-post)
|
||||
- [Deleting post](#deleting-post)
|
||||
- [Merging posts](#merging-posts)
|
||||
- [Rating post](#rating-post)
|
||||
|
@ -951,6 +952,29 @@ data.
|
|||
|
||||
Retrieves information about an existing post.
|
||||
|
||||
## Getting around post
|
||||
- **Request**
|
||||
|
||||
`GET /post/<id>/around`
|
||||
|
||||
- **Output**
|
||||
|
||||
```json5
|
||||
{
|
||||
"prev": <post-resource>,
|
||||
"next": <post-resource>
|
||||
}
|
||||
```
|
||||
|
||||
- **Errors**
|
||||
|
||||
- the post does not exist
|
||||
- privileges are too low
|
||||
|
||||
- **Description**
|
||||
|
||||
Retrieves information about posts that are before or after an existing post.
|
||||
|
||||
## Deleting post
|
||||
- **Request**
|
||||
|
||||
|
|
|
@ -34,33 +34,79 @@ and Docker Compose (version 1.6.0 or greater) already installed.
|
|||
Read the comments to guide you. Note that `.env` should be in the root
|
||||
directory of this repository.
|
||||
|
||||
### Running the Application
|
||||
4. Pull the containers:
|
||||
|
||||
Download containers:
|
||||
```console
|
||||
user@host:szuru$ docker-compose pull
|
||||
```
|
||||
This pulls the latest containers from docker.io:
|
||||
```console
|
||||
user@host:szuru$ docker-compose pull
|
||||
```
|
||||
|
||||
For first run, it is recommended to start the database separately:
|
||||
```console
|
||||
user@host:szuru$ docker-compose up -d sql
|
||||
```
|
||||
If you have modified the application's source and would like to manually
|
||||
build it, follow the instructions in [**Building**](#Building) instead,
|
||||
then read here once you're done.
|
||||
|
||||
To start all containers:
|
||||
```console
|
||||
user@host:szuru$ docker-compose up -d
|
||||
```
|
||||
5. Run it!
|
||||
|
||||
To view/monitor the application logs:
|
||||
```console
|
||||
user@host:szuru$ docker-compose logs -f
|
||||
# (CTRL+C to exit)
|
||||
```
|
||||
For first run, it is recommended to start the database separately:
|
||||
```console
|
||||
user@host:szuru$ docker-compose up -d sql
|
||||
```
|
||||
|
||||
To start all containers:
|
||||
```console
|
||||
user@host:szuru$ docker-compose up -d
|
||||
```
|
||||
|
||||
To view/monitor the application logs:
|
||||
```console
|
||||
user@host:szuru$ docker-compose logs -f
|
||||
# (CTRL+C to exit)
|
||||
```
|
||||
|
||||
### Building
|
||||
|
||||
1. Edit `docker-compose.yml` to tell Docker to build instead of pull containers:
|
||||
|
||||
```diff yaml
|
||||
...
|
||||
server:
|
||||
- image: szurubooru/server:latest
|
||||
+ build: server
|
||||
...
|
||||
client:
|
||||
- image: szurubooru/client:latest
|
||||
+ build: client
|
||||
...
|
||||
```
|
||||
|
||||
You can choose to build either one from source.
|
||||
|
||||
2. Build the containers:
|
||||
|
||||
```console
|
||||
user@host:szuru$ docker-compose build
|
||||
```
|
||||
|
||||
That will attempt to build both containers, but you can specify `client`
|
||||
or `server` to make it build only one.
|
||||
|
||||
If `docker-compose build` spits out:
|
||||
|
||||
```
|
||||
ERROR: Service 'server' failed to build: failed to parse platform : "" is an invalid component of "": platform specifier component must match "^[A-Za-z0-9_-]+$": invalid argument
|
||||
```
|
||||
|
||||
...you will need to export Docker BuildKit flags:
|
||||
|
||||
```console
|
||||
user@host:szuru$ export DOCKER_BUILDKIT=1; export COMPOSE_DOCKER_CLI_BUILD=1
|
||||
```
|
||||
|
||||
...and run `docker-compose build` again.
|
||||
|
||||
*Note: If your changes are not taking effect in your builds, consider building
|
||||
with `--no-cache`.*
|
||||
|
||||
To stop all containers:
|
||||
```console
|
||||
user@host:szuru$ docker-compose down
|
||||
```
|
||||
|
||||
### Additional Features
|
||||
|
||||
|
|
|
@ -10,6 +10,12 @@ BUILD_INFO=latest
|
|||
# otherwise the port specified here will be publicly accessible
|
||||
PORT=8080
|
||||
|
||||
# How many waitress threads to start
|
||||
# 4 is the default amount of threads. If you experience performance
|
||||
# degradation with a large number of posts, increasing this may
|
||||
# improve performance, since waitress is most likely clogging up with Tasks.
|
||||
THREADS=4
|
||||
|
||||
# URL base to run szurubooru under
|
||||
# See "Additional Features" section in INSTALL.md
|
||||
BASE_URL=/
|
||||
|
|
|
@ -22,14 +22,15 @@ services:
|
|||
#POSTGRES_DB: defaults to same as POSTGRES_USER
|
||||
#POSTGRES_PORT: 5432
|
||||
#LOG_SQL: 0 (1 for verbose SQL logs)
|
||||
THREADS:
|
||||
volumes:
|
||||
- "${MOUNT_DATA}:/data"
|
||||
- "./server/config.yaml:/opt/app/config.yaml"
|
||||
|
||||
client:
|
||||
#image: szurubooru/client:latest
|
||||
image: szuruclient:latest
|
||||
#build: ./client
|
||||
#image: szuruclient:latest
|
||||
build: ./client
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
- server
|
||||
|
|
|
@ -7,8 +7,13 @@ WORKDIR /opt/app
|
|||
RUN apk --no-cache add \
|
||||
python3 \
|
||||
python3-dev \
|
||||
ffmpeg \
|
||||
py3-pip \
|
||||
build-base \
|
||||
libheif \
|
||||
libheif-dev \
|
||||
libavif \
|
||||
libavif-dev \
|
||||
ffmpeg \
|
||||
# from requirements.txt:
|
||||
py3-yaml \
|
||||
py3-psycopg2 \
|
||||
|
@ -19,18 +24,13 @@ RUN apk --no-cache add \
|
|||
py3-pynacl \
|
||||
py3-tz \
|
||||
py3-pyrfc3339 \
|
||||
build-base \
|
||||
&& apk --no-cache add \
|
||||
libheif \
|
||||
libavif \
|
||||
libheif-dev \
|
||||
libavif-dev \
|
||||
&& pip3 install --no-cache-dir --disable-pip-version-check \
|
||||
alembic \
|
||||
"alembic>=0.8.5" \
|
||||
"coloredlogs==5.0" \
|
||||
"pyheif==0.6.1" \
|
||||
"heif-image-plugin>=0.3.2" \
|
||||
youtube_dl \
|
||||
pillow-avif-plugin \
|
||||
pyheif-pillow-opener \
|
||||
"pillow-avif-plugin>=1.1.0" \
|
||||
&& apk --no-cache del py3-pip
|
||||
|
||||
COPY ./ /opt/app/
|
||||
|
@ -83,6 +83,9 @@ ARG PORT=6666
|
|||
ENV PORT=${PORT}
|
||||
EXPOSE ${PORT}
|
||||
|
||||
ARG THREADS=4
|
||||
ENV THREADS=${THREADS}
|
||||
|
||||
VOLUME ["/data/"]
|
||||
|
||||
ARG DOCKER_REPO
|
||||
|
|
|
@ -4,5 +4,5 @@ cd /opt/app
|
|||
|
||||
alembic upgrade head
|
||||
|
||||
echo "Starting szurubooru API on port ${PORT}"
|
||||
exec waitress-serve-3 --port ${PORT} szurubooru.facade:app
|
||||
echo "Starting szurubooru API on port ${PORT} - Running on ${THREADS} threads"
|
||||
exec waitress-serve-3 --port ${PORT} --threads ${THREADS} szurubooru.facade:app
|
||||
|
|
|
@ -1,14 +1,15 @@
|
|||
alembic>=0.8.5
|
||||
pyyaml>=3.11
|
||||
psycopg2-binary>=2.6.1
|
||||
SQLAlchemy>=1.0.12, <1.4
|
||||
coloredlogs==5.0
|
||||
certifi>=2017.11.5
|
||||
coloredlogs==5.0
|
||||
heif-image-plugin==0.3.2
|
||||
numpy>=1.8.2
|
||||
pillow>=4.3.0
|
||||
pynacl>=1.2.1
|
||||
pytz>=2018.3
|
||||
pyRFC3339>=1.0
|
||||
pillow-avif-plugin>=1.1.0
|
||||
pyheif-pillow-opener>=0.1.0
|
||||
pillow>=4.3.0
|
||||
psycopg2-binary>=2.6.1
|
||||
pyheif==0.6.1
|
||||
pynacl>=1.2.1
|
||||
pyRFC3339>=1.0
|
||||
pytz>=2018.3
|
||||
pyyaml>=3.11
|
||||
SQLAlchemy>=1.0.12, <1.4
|
||||
youtube_dl
|
||||
|
|
|
@ -91,6 +91,15 @@ def reset_filenames() -> None:
|
|||
rename_in_dir("posts/custom-thumbnails/")
|
||||
|
||||
|
||||
def regenerate_thumbnails() -> None:
|
||||
for post in db.session.query(model.Post).all():
|
||||
print("Generating tumbnail for post %d ..." % post.post_id, end="\r")
|
||||
try:
|
||||
postfuncs.generate_post_thumbnail(post)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser_top = ArgumentParser(
|
||||
description="Collection of CLI commands for an administrator to use",
|
||||
|
@ -114,6 +123,12 @@ def main() -> None:
|
|||
help="reset and rename the content and thumbnail "
|
||||
"filenames in case of a lost/changed secret key",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--regenerate-thumbnails",
|
||||
action="store_true",
|
||||
help="regenerate the thumbnails for posts if the "
|
||||
"thumbnail files are missing",
|
||||
)
|
||||
command = parser_top.parse_args()
|
||||
|
||||
try:
|
||||
|
@ -123,6 +138,8 @@ def main() -> None:
|
|||
check_audio()
|
||||
elif command.reset_filenames:
|
||||
reset_filenames()
|
||||
elif command.regenerate_thumbnails:
|
||||
regenerate_thumbnails()
|
||||
except errors.BaseError as e:
|
||||
print(e, file=stderr)
|
||||
|
||||
|
|
|
@ -33,7 +33,7 @@ def _docker_config() -> Dict:
|
|||
"show_sql": int(os.getenv("LOG_SQL", 0)),
|
||||
"data_url": os.getenv("DATA_URL", "data/"),
|
||||
"data_dir": "/data/",
|
||||
"database": "postgres://%(user)s:%(pass)s@%(host)s:%(port)d/%(db)s"
|
||||
"database": "postgresql://%(user)s:%(pass)s@%(host)s:%(port)d/%(db)s"
|
||||
% {
|
||||
"user": os.getenv("POSTGRES_USER"),
|
||||
"pass": os.getenv("POSTGRES_PASSWORD"),
|
||||
|
|
|
@ -135,7 +135,7 @@ _live_migrations = (
|
|||
|
||||
|
||||
def create_app() -> Callable[[Any, Any], Any]:
|
||||
""" Create a WSGI compatible App object. """
|
||||
"""Create a WSGI compatible App object."""
|
||||
validate_config()
|
||||
coloredlogs.install(fmt="[%(asctime)-15s] %(name)s %(message)s")
|
||||
if config.config["debug"]:
|
||||
|
|
|
@ -25,7 +25,7 @@ RANK_MAP = OrderedDict(
|
|||
|
||||
|
||||
def get_password_hash(salt: str, password: str) -> Tuple[str, int]:
|
||||
""" Retrieve argon2id password hash. """
|
||||
"""Retrieve argon2id password hash."""
|
||||
return (
|
||||
pwhash.argon2id.str(
|
||||
(config.config["secret"] + salt + password).encode("utf8")
|
||||
|
@ -37,7 +37,7 @@ def get_password_hash(salt: str, password: str) -> Tuple[str, int]:
|
|||
def get_sha256_legacy_password_hash(
|
||||
salt: str, password: str
|
||||
) -> Tuple[str, int]:
|
||||
""" Retrieve old-style sha256 password hash. """
|
||||
"""Retrieve old-style sha256 password hash."""
|
||||
digest = hashlib.sha256()
|
||||
digest.update(config.config["secret"].encode("utf8"))
|
||||
digest.update(salt.encode("utf8"))
|
||||
|
@ -46,7 +46,7 @@ def get_sha256_legacy_password_hash(
|
|||
|
||||
|
||||
def get_sha1_legacy_password_hash(salt: str, password: str) -> Tuple[str, int]:
|
||||
""" Retrieve old-style sha1 password hash. """
|
||||
"""Retrieve old-style sha1 password hash."""
|
||||
digest = hashlib.sha1()
|
||||
digest.update(b"1A2/$_4xVa")
|
||||
digest.update(salt.encode("utf8"))
|
||||
|
@ -125,7 +125,7 @@ def verify_privilege(user: model.User, privilege_name: str) -> None:
|
|||
|
||||
|
||||
def generate_authentication_token(user: model.User) -> str:
|
||||
""" Generate nonguessable challenge (e.g. links in password reminder). """
|
||||
"""Generate nonguessable challenge (e.g. links in password reminder)."""
|
||||
assert user
|
||||
digest = hashlib.md5()
|
||||
digest.update(config.config["secret"].encode("utf8"))
|
||||
|
|
|
@ -4,16 +4,13 @@ from datetime import datetime
|
|||
from io import BytesIO
|
||||
from typing import Any, Callable, List, Optional, Set, Tuple
|
||||
|
||||
import HeifImagePlugin
|
||||
import numpy as np
|
||||
import pillow_avif
|
||||
import pyheif
|
||||
from PIL import Image
|
||||
from pyheif_pillow_opener import register_heif_opener
|
||||
|
||||
from szurubooru import config, errors
|
||||
|
||||
register_heif_opener()
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Math based on paper from H. Chi Wong, Marshall Bern and David Goldberg
|
||||
|
|
|
@ -7,6 +7,8 @@ import subprocess
|
|||
from io import BytesIO
|
||||
from typing import List
|
||||
|
||||
import HeifImagePlugin
|
||||
import pillow_avif
|
||||
from PIL import Image as PILImage
|
||||
|
||||
from szurubooru import errors
|
||||
|
@ -277,10 +279,10 @@ class Image:
|
|||
proc = subprocess.Popen(
|
||||
cli,
|
||||
stdout=subprocess.PIPE,
|
||||
stdin=subprocess.PIPE,
|
||||
stdin=subprocess.DEVNULL,
|
||||
stderr=subprocess.PIPE,
|
||||
)
|
||||
out, err = proc.communicate(input=self.content)
|
||||
out, err = proc.communicate()
|
||||
if proc.returncode != 0:
|
||||
logger.warning(
|
||||
"Failed to execute ffmpeg command (cli=%r, err=%r)",
|
||||
|
|
|
@ -36,7 +36,7 @@ def get_mime_type(content: bytes) -> str:
|
|||
if content[0:4] == b"\x1A\x45\xDF\xA3":
|
||||
return "video/webm"
|
||||
|
||||
if content[4:12] in (b"ftypisom", b"ftypiso5", b"ftypmp42", b"ftypM4V "):
|
||||
if content[4:12] in (b"ftypisom", b"ftypiso5", b"ftypiso6", b"ftypmp42", b"ftypM4V "):
|
||||
return "video/mp4"
|
||||
|
||||
return "application/octet-stream"
|
||||
|
|
|
@ -39,7 +39,7 @@ def download(url: str, use_video_downloader: bool = False) -> bytes:
|
|||
length_tally = 0
|
||||
try:
|
||||
with urllib.request.urlopen(request) as handle:
|
||||
while (chunk := handle.read(_dl_chunk_size)) :
|
||||
while chunk := handle.read(_dl_chunk_size):
|
||||
length_tally += len(chunk)
|
||||
if length_tally > config.config["max_dl_filesize"]:
|
||||
raise DownloadTooLargeError(
|
||||
|
|
|
@ -83,12 +83,12 @@ def flip(source: Dict[Any, Any]) -> Dict[Any, Any]:
|
|||
|
||||
|
||||
def is_valid_email(email: Optional[str]) -> bool:
|
||||
""" Return whether given email address is valid or empty. """
|
||||
"""Return whether given email address is valid or empty."""
|
||||
return not email or re.match(r"^[^@]*@[^@]*\.[^@]*$", email) is not None
|
||||
|
||||
|
||||
class dotdict(dict):
|
||||
""" dot.notation access to dictionary attributes. """
|
||||
"""dot.notation access to dictionary attributes."""
|
||||
|
||||
def __getattr__(self, attr: str) -> Any:
|
||||
return self.get(attr)
|
||||
|
@ -98,7 +98,7 @@ class dotdict(dict):
|
|||
|
||||
|
||||
def parse_time_range(value: str) -> Tuple[datetime, datetime]:
|
||||
""" Return tuple containing min/max time for given text representation. """
|
||||
"""Return tuple containing min/max time for given text representation."""
|
||||
one_day = timedelta(days=1)
|
||||
one_second = timedelta(seconds=1)
|
||||
almost_one_day = one_day - one_second
|
||||
|
|
|
@ -7,7 +7,7 @@ from szurubooru.rest.errors import HttpBadRequest
|
|||
|
||||
|
||||
def _authenticate_basic_auth(username: str, password: str) -> model.User:
|
||||
""" Try to authenticate user. Throw AuthError for invalid users. """
|
||||
"""Try to authenticate user. Throw AuthError for invalid users."""
|
||||
user = users.get_user_by_name(username)
|
||||
if not auth.is_valid_password(user, password):
|
||||
raise errors.AuthError("Invalid password.")
|
||||
|
@ -17,7 +17,7 @@ def _authenticate_basic_auth(username: str, password: str) -> model.User:
|
|||
def _authenticate_token(
|
||||
username: str, token: str
|
||||
) -> Tuple[model.User, model.UserToken]:
|
||||
""" Try to authenticate user. Throw AuthError for invalid users. """
|
||||
"""Try to authenticate user. Throw AuthError for invalid users."""
|
||||
user = users.get_user_by_name(username)
|
||||
user_token = user_tokens.get_by_user_and_token(user, token)
|
||||
if not auth.is_valid_token(user_token):
|
||||
|
@ -72,7 +72,7 @@ def _get_user(ctx: rest.Context, bump_login: bool) -> Optional[model.User]:
|
|||
|
||||
|
||||
def process_request(ctx: rest.Context) -> None:
|
||||
""" Bind the user to request. Update last login time if needed. """
|
||||
"""Bind the user to request. Update last login time if needed."""
|
||||
bump_login = ctx.get_param_as_bool("bump-login", default=False)
|
||||
auth_user = _get_user(ctx, bump_login)
|
||||
if auth_user:
|
||||
|
|
|
@ -11,7 +11,7 @@ from szurubooru.rest import context, errors, middleware, routes
|
|||
|
||||
|
||||
def _json_serializer(obj: Any) -> str:
|
||||
""" JSON serializer for objects not serializable by default JSON code """
|
||||
"""JSON serializer for objects not serializable by default JSON code"""
|
||||
if isinstance(obj, datetime):
|
||||
serial = obj.isoformat("T") + "Z"
|
||||
return serial
|
||||
|
|
Loading…
Reference in New Issue