Merge branch 'main' into dev

This commit is contained in:
Michael Shamoon 2022-09-26 07:55:49 -07:00
commit ef2789cf57
31 changed files with 1295 additions and 1045 deletions

View File

@ -1,3 +1,14 @@
autolabeler:
- label: "bug"
branch:
- '/^fix/'
title:
- "/^fix/i"
- label: "enhancement"
branch:
- '/^feature/'
title:
- "/^feature/i"
categories: categories:
- title: 'Breaking Changes' - title: 'Breaking Changes'
labels: labels:
@ -15,9 +26,15 @@ categories:
- 'chore' - 'chore'
- 'deployment' - 'deployment'
- 'translation' - 'translation'
- 'ci-cd'
- title: 'Dependencies' - title: 'Dependencies'
collapse-after: 3 collapse-after: 3
label: 'dependencies' label: 'dependencies'
- title: 'All App Changes'
labels:
- 'frontend'
- 'backend'
collapse-after: 0
include-labels: include-labels:
- 'enhancement' - 'enhancement'
- 'bug' - 'bug'
@ -25,11 +42,12 @@ include-labels:
- 'deployment' - 'deployment'
- 'translation' - 'translation'
- 'dependencies' - 'dependencies'
replacers: # Changes "Feature: Update checker" to "Update checker" - 'documentation'
- search: '/Feature:|Feat:|\[feature\]/gi' - 'frontend'
replace: '' - 'backend'
- 'ci-cd'
category-template: '### $TITLE' category-template: '### $TITLE'
change-template: '- $TITLE [@$AUTHOR](https://github.com/$AUTHOR) ([#$NUMBER]($URL))' change-template: '- $TITLE @$AUTHOR ([#$NUMBER]($URL))'
change-title-escapes: '\<*_&#@' change-title-escapes: '\<*_&#@'
template: | template: |
## paperless-ngx $RESOLVED_VERSION ## paperless-ngx $RESOLVED_VERSION

View File

@ -1,167 +1,41 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import functools
import json import json
import logging import logging
import os import os
import re
import shutil import shutil
import subprocess import subprocess
from argparse import ArgumentParser from argparse import ArgumentParser
from typing import Dict from typing import Dict
from typing import Final from typing import Final
from typing import List from typing import List
from urllib.parse import quote
import requests
from common import get_log_level from common import get_log_level
from github import ContainerPackage
from github import GithubBranchApi
from github import GithubContainerRegistryApi
logger = logging.getLogger("cleanup-tags") logger = logging.getLogger("cleanup-tags")
class ContainerPackage: class DockerManifest2:
def __init__(self, data: Dict): """
Data class wrapping the Docker Image Manifest Version 2.
See https://docs.docker.com/registry/spec/manifest-v2-2/
"""
def __init__(self, data: Dict) -> None:
self._data = data self._data = data
self.name = self._data["name"] # This is the sha256: digest string. Corresponds to Github API name
self.id = self._data["id"] # if the package is an untagged package
self.url = self._data["url"] self.digest = self._data["digest"]
self.tags = self._data["metadata"]["container"]["tags"] platform_data_os = self._data["platform"]["os"]
platform_arch = self._data["platform"]["architecture"]
@functools.cached_property platform_variant = self._data["platform"].get(
def untagged(self) -> bool: "variant",
return len(self.tags) == 0 "",
@functools.cache
def tag_matches(self, pattern: str) -> bool:
for tag in self.tags:
if re.match(pattern, tag) is not None:
return True
return False
def __repr__(self):
return f"Package {self.name}"
class GithubContainerRegistry:
def __init__(
self,
session: requests.Session,
token: str,
owner_or_org: str,
):
self._session: requests.Session = session
self._token = token
self._owner_or_org = owner_or_org
# https://docs.github.com/en/rest/branches/branches
self._BRANCHES_ENDPOINT = "https://api.github.com/repos/{OWNER}/{REPO}/branches"
if self._owner_or_org == "paperless-ngx":
# https://docs.github.com/en/rest/packages#get-all-package-versions-for-a-package-owned-by-an-organization
self._PACKAGES_VERSIONS_ENDPOINT = "https://api.github.com/orgs/{ORG}/packages/{PACKAGE_TYPE}/{PACKAGE_NAME}/versions"
# https://docs.github.com/en/rest/packages#delete-package-version-for-an-organization
self._PACKAGE_VERSION_DELETE_ENDPOINT = "https://api.github.com/orgs/{ORG}/packages/{PACKAGE_TYPE}/{PACKAGE_NAME}/versions/{PACKAGE_VERSION_ID}"
else:
# https://docs.github.com/en/rest/packages#get-all-package-versions-for-a-package-owned-by-the-authenticated-user
self._PACKAGES_VERSIONS_ENDPOINT = "https://api.github.com/user/packages/{PACKAGE_TYPE}/{PACKAGE_NAME}/versions"
# https://docs.github.com/en/rest/packages#delete-a-package-version-for-the-authenticated-user
self._PACKAGE_VERSION_DELETE_ENDPOINT = "https://api.github.com/user/packages/{PACKAGE_TYPE}/{PACKAGE_NAME}/versions/{PACKAGE_VERSION_ID}"
def __enter__(self):
"""
Sets up the required headers for auth and response
type from the API
"""
self._session.headers.update(
{
"Accept": "application/vnd.github.v3+json",
"Authorization": f"token {self._token}",
},
) )
return self self.platform = f"{platform_data_os}/{platform_arch}{platform_variant}"
def __exit__(self, exc_type, exc_val, exc_tb):
"""
Ensures the authorization token is cleaned up no matter
the reason for the exit
"""
if "Accept" in self._session.headers:
del self._session.headers["Accept"]
if "Authorization" in self._session.headers:
del self._session.headers["Authorization"]
def _read_all_pages(self, endpoint):
"""
Internal function to read all pages of an endpoint, utilizing the
next.url until exhausted
"""
internal_data = []
while True:
resp = self._session.get(endpoint)
if resp.status_code == 200:
internal_data += resp.json()
if "next" in resp.links:
endpoint = resp.links["next"]["url"]
else:
logger.debug("Exiting pagination loop")
break
else:
logger.warning(f"Request to {endpoint} return HTTP {resp.status_code}")
break
return internal_data
def get_branches(self, repo: str):
"""
Returns all current branches of the given repository
"""
endpoint = self._BRANCHES_ENDPOINT.format(OWNER=self._owner_or_org, REPO=repo)
internal_data = self._read_all_pages(endpoint)
return internal_data
def filter_branches_by_name_pattern(self, branch_data, pattern: str):
"""
Filters the given list of branches to those which start with the given
pattern. Future enhancement could use regex patterns instead.
"""
matches = {}
for branch in branch_data:
if branch["name"].startswith(pattern):
matches[branch["name"]] = branch
return matches
def get_package_versions(
self,
package_name: str,
package_type: str = "container",
) -> List[ContainerPackage]:
"""
Returns all the versions of a given package (container images) from
the API
"""
package_name = quote(package_name, safe="")
endpoint = self._PACKAGES_VERSIONS_ENDPOINT.format(
ORG=self._owner_or_org,
PACKAGE_TYPE=package_type,
PACKAGE_NAME=package_name,
)
pkgs = []
for data in self._read_all_pages(endpoint):
pkgs.append(ContainerPackage(data))
return pkgs
def delete_package_version(self, package_data: ContainerPackage):
"""
Deletes the given package version from the GHCR
"""
resp = self._session.delete(package_data.url)
if resp.status_code != 204:
logger.warning(
f"Request to delete {package_data.url} returned HTTP {resp.status_code}",
)
def _main(): def _main():
@ -187,6 +61,15 @@ def _main():
help="If provided, delete untagged containers as well", help="If provided, delete untagged containers as well",
) )
# If given, the package is assumed to be a multi-arch manifest. Cache packages are
# not multi-arch, all other types are
parser.add_argument(
"--is-manifest",
action="store_true",
default=False,
help="If provided, the package is assumed to be a multi-arch manifest following schema v2",
)
# Allows configuration of log level for debugging # Allows configuration of log level for debugging
parser.add_argument( parser.add_argument(
"--loglevel", "--loglevel",
@ -194,6 +77,12 @@ def _main():
help="Configures the logging level", help="Configures the logging level",
) )
# Get the name of the package being processed this round
parser.add_argument(
"package",
help="The package to process",
)
args = parser.parse_args() args = parser.parse_args()
logging.basicConfig( logging.basicConfig(
@ -207,181 +96,194 @@ def _main():
repo: Final[str] = os.environ["GITHUB_REPOSITORY"] repo: Final[str] = os.environ["GITHUB_REPOSITORY"]
gh_token: Final[str] = os.environ["TOKEN"] gh_token: Final[str] = os.environ["TOKEN"]
with requests.session() as sess: # Find all branches named feature-*
with GithubContainerRegistry(sess, gh_token, repo_owner) as gh_api: # Note: Only relevant to the main application, but simpler to
# leave in for all packages
with GithubBranchApi(gh_token) as branch_api:
feature_branches = {}
for branch in branch_api.get_branches(
repo=repo,
):
if branch.name.startswith("feature-"):
logger.debug(f"Found feature branch {branch.name}")
feature_branches[branch.name] = branch
# Step 1 - Get branch information logger.info(f"Located {len(feature_branches)} feature branches")
# Step 1.1 - Locate all branches of the repo with GithubContainerRegistryApi(gh_token, repo_owner) as container_api:
all_branches = gh_api.get_branches("paperless-ngx") # Get the information about all versions of the given package
logger.info(f"Located {len(all_branches)} branches of {repo_owner}/{repo} ") all_package_versions: List[
ContainerPackage
] = container_api.get_package_versions(args.package)
# Step 1.2 - Filter branches to those starting with "feature-" all_pkgs_tags_to_version: Dict[str, ContainerPackage] = {}
feature_branches = gh_api.filter_branches_by_name_pattern( for pkg in all_package_versions:
all_branches, for tag in pkg.tags:
"feature-", all_pkgs_tags_to_version[tag] = pkg
) logger.info(
logger.info(f"Located {len(feature_branches)} feature branches") f"Located {len(all_package_versions)} versions of package {args.package}",
)
# Step 2 - Deal with package information # Filter to packages which are tagged with feature-*
for package_name in ["paperless-ngx", "paperless-ngx/builder/cache/app"]: packages_tagged_feature: List[ContainerPackage] = []
for package in all_package_versions:
if package.tag_matches("feature-"):
packages_tagged_feature.append(package)
# Step 2.1 - Location all versions of the given package feature_pkgs_tags_to_versions: Dict[str, ContainerPackage] = {}
all_package_versions = gh_api.get_package_versions(package_name) for pkg in packages_tagged_feature:
for tag in pkg.tags:
feature_pkgs_tags_to_versions[tag] = pkg
# Faster lookup, map the tag to their container logger.info(
all_pkgs_tags_to_version = {} f'Located {len(feature_pkgs_tags_to_versions)} versions of package {args.package} tagged "feature-"',
for pkg in all_package_versions: )
for tag in pkg.tags:
all_pkgs_tags_to_version[tag] = pkg # All the feature tags minus all the feature branches leaves us feature tags
# with no corresponding branch
tags_to_delete = list(
set(feature_pkgs_tags_to_versions.keys()) - set(feature_branches.keys()),
)
# All the tags minus the set of going to be deleted tags leaves us the
# tags which will be kept around
tags_to_keep = list(
set(all_pkgs_tags_to_version.keys()) - set(tags_to_delete),
)
logger.info(
f"Located {len(tags_to_delete)} versions of package {args.package} to delete",
)
# Delete certain package versions for which no branch existed
for tag_to_delete in tags_to_delete:
package_version_info = feature_pkgs_tags_to_versions[tag_to_delete]
if args.delete:
logger.info( logger.info(
f"Located {len(all_package_versions)} versions of package {package_name}", f"Deleting {tag_to_delete} (id {package_version_info.id})",
)
container_api.delete_package_version(
package_version_info,
) )
# Step 2.2 - Location package versions which have a tag of "feature-" else:
packages_tagged_feature = [] logger.info(
f"Would delete {tag_to_delete} (id {package_version_info.id})",
)
# Deal with untagged package versions
if args.untagged:
logger.info("Handling untagged image packages")
if not args.is_manifest:
# If the package is not a multi-arch manifest, images without tags are safe to delete.
# They are not referred to by anything. This will leave all with at least 1 tag
for package in all_package_versions: for package in all_package_versions:
if package.tag_matches("feature-"): if package.untagged:
packages_tagged_feature.append(package) if args.delete:
logger.info(
f'Located {len(packages_tagged_feature)} versions of package {package_name} tagged "feature-"',
)
# Faster lookup, map feature- tags to their container
feature_pkgs_tags_to_versions = {}
for pkg in packages_tagged_feature:
for tag in pkg.tags:
feature_pkgs_tags_to_versions[tag] = pkg
# Step 2.3 - Determine which package versions have no matching branch and which tags we're keeping
tags_to_delete = list(
set(feature_pkgs_tags_to_versions.keys())
- set(feature_branches.keys()),
)
tags_to_keep = list(
set(all_pkgs_tags_to_version.keys()) - set(tags_to_delete),
)
logger.info(
f"Located {len(tags_to_delete)} versions of package {package_name} to delete",
)
# Step 2.4 - Delete certain package versions
for tag_to_delete in tags_to_delete:
package_version_info = feature_pkgs_tags_to_versions[tag_to_delete]
if args.delete:
logger.info(
f"Deleting {tag_to_delete} (id {package_version_info.id})",
)
gh_api.delete_package_version(
package_version_info,
)
else:
logger.info(
f"Would delete {tag_to_delete} (id {package_version_info.id})",
)
# Step 3 - Deal with untagged and dangling packages
if args.untagged:
"""
Ok, bear with me, these are annoying.
Our images are multi-arch, so the manifest is more like a pointer to a sha256 digest.
These images are untagged, but pointed to, and so should not be removed (or every pull fails).
So for each image getting kept, parse the manifest to find the digest(s) it points to. Then
remove those from the list of untagged images. The final result is the untagged, not pointed to
version which should be safe to remove.
Example:
Tag: ghcr.io/paperless-ngx/paperless-ngx:1.7.1 refers to
amd64: sha256:b9ed4f8753bbf5146547671052d7e91f68cdfc9ef049d06690b2bc866fec2690
armv7: sha256:81605222df4ba4605a2ba4893276e5d08c511231ead1d5da061410e1bbec05c3
arm64: sha256:374cd68db40734b844705bfc38faae84cc4182371de4bebd533a9a365d5e8f3b
each of which appears as untagged image
"""
# Step 3.1 - Simplify the untagged data, mapping name (which is a digest) to the version
untagged_versions = {}
for x in all_package_versions:
if x.untagged:
untagged_versions[x.name] = x
skips = 0
# Extra security to not delete on an unexpected error
actually_delete = True
logger.info(
f"Located {len(tags_to_keep)} tags of package {package_name} to keep",
)
# Step 3.2 - Parse manifests to locate digests pointed to
for tag in tags_to_keep:
full_name = f"ghcr.io/{repo_owner}/{package_name}:{tag}"
logger.info(f"Checking manifest for {full_name}")
try:
proc = subprocess.run(
[
shutil.which("docker"),
"manifest",
"inspect",
full_name,
],
capture_output=True,
)
manifest_list = json.loads(proc.stdout)
for manifest in manifest_list["manifests"]:
digest = manifest["digest"]
platform_data_os = manifest["platform"]["os"]
platform_arch = manifest["platform"]["architecture"]
platform_variant = manifest["platform"].get(
"variant",
"",
)
platform = f"{platform_data_os}/{platform_arch}{platform_variant}"
if digest in untagged_versions:
logger.debug(
f"Skipping deletion of {digest}, referred to by {full_name} for {platform}",
)
del untagged_versions[digest]
skips += 1
except json.decoder.JSONDecodeError as err:
# This is probably for a cache image, which isn't a multi-arch digest
# These are ok to delete all on
logger.debug(f"{err} on {full_name}")
continue
except Exception as err:
actually_delete = False
logger.exception(err)
continue
logger.info(f"Skipping deletion of {skips} packages")
# Step 3.3 - Delete the untagged and not pointed at packages
logger.info(f"Deleting untagged packages of {package_name}")
for to_delete_name in untagged_versions:
to_delete_version = untagged_versions[to_delete_name]
if args.delete and actually_delete:
logger.info( logger.info(
f"Deleting id {to_delete_version.id} named {to_delete_version.name}", f"Deleting id {package.id} named {package.name}",
) )
gh_api.delete_package_version( container_api.delete_package_version(
to_delete_version, package,
) )
else: else:
logger.info( logger.info(
f"Would delete {to_delete_name} (id {to_delete_version.id})", f"Would delete {package.name} (id {package.id})",
) )
else: else:
logger.info("Leaving untagged images untouched") logger.info(
f"Not deleting tag {package.tags[0]} of package {args.package}",
)
else:
"""
Ok, bear with me, these are annoying.
Our images are multi-arch, so the manifest is more like a pointer to a sha256 digest.
These images are untagged, but pointed to, and so should not be removed (or every pull fails).
So for each image getting kept, parse the manifest to find the digest(s) it points to. Then
remove those from the list of untagged images. The final result is the untagged, not pointed to
version which should be safe to remove.
Example:
Tag: ghcr.io/paperless-ngx/paperless-ngx:1.7.1 refers to
amd64: sha256:b9ed4f8753bbf5146547671052d7e91f68cdfc9ef049d06690b2bc866fec2690
armv7: sha256:81605222df4ba4605a2ba4893276e5d08c511231ead1d5da061410e1bbec05c3
arm64: sha256:374cd68db40734b844705bfc38faae84cc4182371de4bebd533a9a365d5e8f3b
each of which appears as untagged image, but isn't really.
So from the list of untagged packages, remove those digests. Once all tags which
are being kept are checked, the remaining untagged packages are actually untagged
with no referrals in a manifest to them.
"""
# Simplify the untagged data, mapping name (which is a digest) to the version
untagged_versions = {}
for x in all_package_versions:
if x.untagged:
untagged_versions[x.name] = x
skips = 0
# Extra security to not delete on an unexpected error
actually_delete = True
# Parse manifests to locate digests pointed to
for tag in sorted(tags_to_keep):
full_name = f"ghcr.io/{repo_owner}/{args.package}:{tag}"
logger.info(f"Checking manifest for {full_name}")
try:
proc = subprocess.run(
[
shutil.which("docker"),
"manifest",
"inspect",
full_name,
],
capture_output=True,
)
manifest_list = json.loads(proc.stdout)
for manifest_data in manifest_list["manifests"]:
manifest = DockerManifest2(manifest_data)
if manifest.digest in untagged_versions:
logger.debug(
f"Skipping deletion of {manifest.digest}, referred to by {full_name} for {manifest.platform}",
)
del untagged_versions[manifest.digest]
skips += 1
except Exception as err:
actually_delete = False
logger.exception(err)
logger.info(
f"Skipping deletion of {skips} packages referred to by a manifest",
)
# Step 3.3 - Delete the untagged and not pointed at packages
logger.info(f"Deleting untagged packages of {args.package}")
for to_delete_name in untagged_versions:
to_delete_version = untagged_versions[to_delete_name]
if args.delete and actually_delete:
logger.info(
f"Deleting id {to_delete_version.id} named {to_delete_version.name}",
)
container_api.delete_package_version(
to_delete_version,
)
else:
logger.info(
f"Would delete {to_delete_name} (id {to_delete_version.id})",
)
else:
logger.info("Leaving untagged images untouched")
if __name__ == "__main__": if __name__ == "__main__":

View File

@ -1,6 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import logging import logging
from argparse import ArgumentError
def get_image_tag( def get_image_tag(

227
.github/scripts/github.py vendored Normal file
View File

@ -0,0 +1,227 @@
#!/usr/bin/env python3
"""
This module contains some useful classes for interacting with the Github API.
The full documentation for the API can be found here: https://docs.github.com/en/rest
Mostly, this focusses on two areas, repo branches and repo packages, as the use case
is cleaning up container images which are no longer referred to.
"""
import functools
import logging
import re
import urllib.parse
from typing import Dict
from typing import List
from typing import Optional
import requests
logger = logging.getLogger("github-api")
class _GithubApiBase:
"""
A base class for interacting with the Github API. It
will handle the session and setting authorization headers.
"""
def __init__(self, token: str) -> None:
self._token = token
self._session: Optional[requests.Session] = None
def __enter__(self) -> "_GithubApiBase":
"""
Sets up the required headers for auth and response
type from the API
"""
self._session = requests.Session()
self._session.headers.update(
{
"Accept": "application/vnd.github.v3+json",
"Authorization": f"token {self._token}",
},
)
return self
def __exit__(self, exc_type, exc_val, exc_tb):
"""
Ensures the authorization token is cleaned up no matter
the reason for the exit
"""
if "Accept" in self._session.headers:
del self._session.headers["Accept"]
if "Authorization" in self._session.headers:
del self._session.headers["Authorization"]
# Close the session as well
self._session.close()
self._session = None
def _read_all_pages(self, endpoint):
"""
Helper function to read all pages of an endpoint, utilizing the
next.url until exhausted. Assumes the endpoint returns a list
"""
internal_data = []
while True:
resp = self._session.get(endpoint)
if resp.status_code == 200:
internal_data += resp.json()
if "next" in resp.links:
endpoint = resp.links["next"]["url"]
else:
logger.debug("Exiting pagination loop")
break
else:
logger.warning(f"Request to {endpoint} return HTTP {resp.status_code}")
break
return internal_data
class _EndpointResponse:
"""
For all endpoint JSON responses, store the full
response data, for ease of extending later, if need be.
"""
def __init__(self, data: Dict) -> None:
self._data = data
class GithubBranch(_EndpointResponse):
"""
Simple wrapper for a repository branch, only extracts name information
for now.
"""
def __init__(self, data: Dict) -> None:
super().__init__(data)
self.name = self._data["name"]
class GithubBranchApi(_GithubApiBase):
"""
Wrapper around branch API.
See https://docs.github.com/en/rest/branches/branches
"""
def __init__(self, token: str) -> None:
super().__init__(token)
self._ENDPOINT = "https://api.github.com/repos/{REPO}/branches"
def get_branches(self, repo: str) -> List[GithubBranch]:
"""
Returns all current branches of the given repository owned by the given
owner or organization.
"""
endpoint = self._ENDPOINT.format(REPO=repo)
internal_data = self._read_all_pages(endpoint)
return [GithubBranch(branch) for branch in internal_data]
class ContainerPackage(_EndpointResponse):
"""
Data class wrapping the JSON response from the package related
endpoints
"""
def __init__(self, data: Dict):
super().__init__(data)
# This is a numerical ID, required for interactions with this
# specific package, including deletion of it or restoration
self.id: int = self._data["id"]
# A string name. This might be an actual name or it could be a
# digest string like "sha256:"
self.name: str = self._data["name"]
# URL to the package, including its ID, can be used for deletion
# or restoration without needing to build up a URL ourselves
self.url: str = self._data["url"]
# The list of tags applied to this image. Maybe an empty list
self.tags: List[str] = self._data["metadata"]["container"]["tags"]
@functools.cached_property
def untagged(self) -> bool:
"""
Returns True if the image has no tags applied to it, False otherwise
"""
return len(self.tags) == 0
@functools.cache
def tag_matches(self, pattern: str) -> bool:
"""
Returns True if the image has at least one tag which matches the given regex,
False otherwise
"""
for tag in self.tags:
if re.match(pattern, tag) is not None:
return True
return False
def __repr__(self):
return f"Package {self.name}"
class GithubContainerRegistryApi(_GithubApiBase):
"""
Class wrapper to deal with the Github packages API. This class only deals with
container type packages, the only type published by paperless-ngx.
"""
def __init__(self, token: str, owner_or_org: str) -> None:
super().__init__(token)
self._owner_or_org = owner_or_org
if self._owner_or_org == "paperless-ngx":
# https://docs.github.com/en/rest/packages#get-all-package-versions-for-a-package-owned-by-an-organization
self._PACKAGES_VERSIONS_ENDPOINT = "https://api.github.com/orgs/{ORG}/packages/{PACKAGE_TYPE}/{PACKAGE_NAME}/versions"
# https://docs.github.com/en/rest/packages#delete-package-version-for-an-organization
self._PACKAGE_VERSION_DELETE_ENDPOINT = "https://api.github.com/orgs/{ORG}/packages/{PACKAGE_TYPE}/{PACKAGE_NAME}/versions/{PACKAGE_VERSION_ID}"
else:
# https://docs.github.com/en/rest/packages#get-all-package-versions-for-a-package-owned-by-the-authenticated-user
self._PACKAGES_VERSIONS_ENDPOINT = "https://api.github.com/user/packages/{PACKAGE_TYPE}/{PACKAGE_NAME}/versions"
# https://docs.github.com/en/rest/packages#delete-a-package-version-for-the-authenticated-user
self._PACKAGE_VERSION_DELETE_ENDPOINT = "https://api.github.com/user/packages/{PACKAGE_TYPE}/{PACKAGE_NAME}/versions/{PACKAGE_VERSION_ID}"
def get_package_versions(
self,
package_name: str,
) -> List[ContainerPackage]:
"""
Returns all the versions of a given package (container images) from
the API
"""
package_type: str = "container"
# Need to quote this for slashes in the name
package_name = urllib.parse.quote(package_name, safe="")
endpoint = self._PACKAGES_VERSIONS_ENDPOINT.format(
ORG=self._owner_or_org,
PACKAGE_TYPE=package_type,
PACKAGE_NAME=package_name,
)
pkgs = []
for data in self._read_all_pages(endpoint):
pkgs.append(ContainerPackage(data))
return pkgs
def delete_package_version(self, package_data: ContainerPackage):
"""
Deletes the given package version from the GHCR
"""
resp = self._session.delete(package_data.url)
if resp.status_code != 204:
logger.warning(
f"Request to delete {package_data.url} returned HTTP {resp.status_code}",
)

View File

@ -452,7 +452,7 @@ jobs:
- -
name: Create Release and Changelog name: Create Release and Changelog
id: create-release id: create-release
uses: release-drafter/release-drafter@v5 uses: paperless-ngx/release-drafter@master
with: with:
name: Paperless-ngx ${{ steps.get_version.outputs.version }} name: Paperless-ngx ${{ steps.get_version.outputs.version }}
tag: ${{ steps.get_version.outputs.version }} tag: ${{ steps.get_version.outputs.version }}
@ -492,6 +492,8 @@ jobs:
git branch ${{ needs.publish-release.outputs.version }}-changelog git branch ${{ needs.publish-release.outputs.version }}-changelog
git checkout ${{ needs.publish-release.outputs.version }}-changelog git checkout ${{ needs.publish-release.outputs.version }}-changelog
echo -e "# Changelog\n\n${{ needs.publish-release.outputs.changelog }}\n" > changelog-new.md echo -e "# Changelog\n\n${{ needs.publish-release.outputs.changelog }}\n" > changelog-new.md
echo "Manually linking usernames"
sed -i -r 's|@(.+?) \(\[#|[@\1](https://github.com/\1) ([#|ig' changelog-new.md
CURRENT_CHANGELOG=`tail --lines +2 changelog.md` CURRENT_CHANGELOG=`tail --lines +2 changelog.md`
echo -e "$CURRENT_CHANGELOG" >> changelog-new.md echo -e "$CURRENT_CHANGELOG" >> changelog-new.md
mv changelog-new.md changelog.md mv changelog-new.md changelog.md

View File

@ -16,6 +16,7 @@ on:
paths: paths:
- ".github/workflows/cleanup-tags.yml" - ".github/workflows/cleanup-tags.yml"
- ".github/scripts/cleanup-tags.py" - ".github/scripts/cleanup-tags.py"
- ".github/scripts/github.py"
- ".github/scripts/common.py" - ".github/scripts/common.py"
jobs: jobs:
@ -45,14 +46,63 @@ jobs:
name: Install requests name: Install requests
run: | run: |
python -m pip install requests python -m pip install requests
# Clean up primary packages
- -
name: Cleanup feature tags name: Cleanup for package "paperless-ngx"
# Only run if the token is not empty
if: "${{ env.TOKEN != '' }}" if: "${{ env.TOKEN != '' }}"
run: | run: |
python ${GITHUB_WORKSPACE}/.github/scripts/cleanup-tags.py --loglevel info --untagged --delete python ${GITHUB_WORKSPACE}/.github/scripts/cleanup-tags.py --loglevel info --untagged --is-manifest --delete "paperless-ngx"
-
name: Cleanup for package "qpdf"
if: "${{ env.TOKEN != '' }}"
run: |
python ${GITHUB_WORKSPACE}/.github/scripts/cleanup-tags.py --loglevel info --untagged --is-manifest --delete "paperless-ngx/builder/qpdf"
-
name: Cleanup for package "pikepdf"
if: "${{ env.TOKEN != '' }}"
run: |
python ${GITHUB_WORKSPACE}/.github/scripts/cleanup-tags.py --loglevel info --untagged --is-manifest --delete "paperless-ngx/builder/pikepdf"
-
name: Cleanup for package "jbig2enc"
if: "${{ env.TOKEN != '' }}"
run: |
python ${GITHUB_WORKSPACE}/.github/scripts/cleanup-tags.py --loglevel info --untagged --is-manifest --delete "paperless-ngx/builder/jbig2enc"
-
name: Cleanup for package "psycopg2"
if: "${{ env.TOKEN != '' }}"
run: |
python ${GITHUB_WORKSPACE}/.github/scripts/cleanup-tags.py --loglevel info --untagged --is-manifest --delete "paperless-ngx/builder/psycopg2"
#
# Clean up registry cache packages
#
-
name: Cleanup for package "builder/cache/app"
if: "${{ env.TOKEN != '' }}"
run: |
python ${GITHUB_WORKSPACE}/.github/scripts/cleanup-tags.py --loglevel info --untagged --delete "paperless-ngx/builder/cache/app"
-
name: Cleanup for package "builder/cache/qpdf"
if: "${{ env.TOKEN != '' }}"
run: |
python ${GITHUB_WORKSPACE}/.github/scripts/cleanup-tags.py --loglevel info --untagged --delete "paperless-ngx/builder/cache/qpdf"
-
name: Cleanup for package "builder/cache/psycopg2"
if: "${{ env.TOKEN != '' }}"
run: |
python ${GITHUB_WORKSPACE}/.github/scripts/cleanup-tags.py --loglevel info --untagged --delete "paperless-ngx/builder/cache/psycopg2"
-
name: Cleanup for package "builder/cache/jbig2enc"
if: "${{ env.TOKEN != '' }}"
run: |
python ${GITHUB_WORKSPACE}/.github/scripts/cleanup-tags.py --loglevel info --untagged --delete "paperless-ngx/builder/cache/jbig2enc"
-
name: Cleanup for package "builder/cache/pikepdf"
if: "${{ env.TOKEN != '' }}"
run: |
python ${GITHUB_WORKSPACE}/.github/scripts/cleanup-tags.py --loglevel info --untagged --delete "paperless-ngx/builder/cache/pikepdf"
- -
name: Check all tags still pull name: Check all tags still pull
run: | run: |
ghcr_name=$(echo "${GITHUB_REPOSITORY}" | awk '{ print tolower($0) }') ghcr_name=$(echo "${GITHUB_REPOSITORY}" | awk '{ print tolower($0) }')
echo "Pulling all tags of ghcr.io/${ghcr_name}"
docker pull --quiet --all-tags ghcr.io/${ghcr_name} docker pull --quiet --all-tags ghcr.io/${ghcr_name}

View File

@ -13,6 +13,9 @@ on:
- main - main
- dev - dev
permissions:
contents: read
env: env:
todo: Todo todo: Todo
done: Done done: Done
@ -24,7 +27,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: github.event_name == 'issues' && (github.event.action == 'opened' || github.event.action == 'reopened') if: github.event_name == 'issues' && (github.event.action == 'opened' || github.event.action == 'reopened')
steps: steps:
- name: Set issue status to ${{ env.todo }} - name: Add issue to project and set status to ${{ env.todo }}
uses: leonsteinhaeuser/project-beta-automations@v1.3.0 uses: leonsteinhaeuser/project-beta-automations@v1.3.0
with: with:
gh_token: ${{ secrets.GH_TOKEN }} gh_token: ${{ secrets.GH_TOKEN }}
@ -35,13 +38,20 @@ jobs:
pr_opened_or_reopened: pr_opened_or_reopened:
name: pr_opened_or_reopened name: pr_opened_or_reopened
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: github.event_name == 'pull_request_target' && (github.event.action == 'opened' || github.event.action == 'reopened') permissions:
# write permission is required for autolabeler
pull-requests: write
if: github.event_name == 'pull_request_target' && (github.event.action == 'opened' || github.event.action == 'reopened') && github.event.pull_request.user.login != 'dependabot'
steps: steps:
- name: Set PR status to ${{ env.in_progress }} - name: Add PR to project and set status to "Needs Review"
uses: leonsteinhaeuser/project-beta-automations@v1.3.0 uses: leonsteinhaeuser/project-beta-automations@v1.3.0
with: with:
gh_token: ${{ secrets.GH_TOKEN }} gh_token: ${{ secrets.GH_TOKEN }}
organization: paperless-ngx organization: paperless-ngx
project_id: 2 project_id: 2
resource_node_id: ${{ github.event.pull_request.node_id }} resource_node_id: ${{ github.event.pull_request.node_id }}
status_value: ${{ env.in_progress }} # Target status status_value: "Needs Review" # Target status
- name: Label PR with release-drafter
uses: release-drafter/release-drafter@v5
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@ -33,6 +33,8 @@ redis = "*"
scikit-learn = "~=1.1" scikit-learn = "~=1.1"
# Pin this until piwheels is building 1.9 (see https://www.piwheels.org/project/scipy/) # Pin this until piwheels is building 1.9 (see https://www.piwheels.org/project/scipy/)
scipy = "==1.8.1" scipy = "==1.8.1"
# https://github.com/paperless-ngx/paperless-ngx/issues/1364
numpy = "==1.22.3"
whitenoise = "~=6.2" whitenoise = "~=6.2"
watchdog = "~=2.1" watchdog = "~=2.1"
whoosh="~=2.7" whoosh="~=2.7"
@ -51,8 +53,8 @@ concurrent-log-handler = "*"
"importlib-resources" = {version = "*", markers = "python_version < '3.9'"} "importlib-resources" = {version = "*", markers = "python_version < '3.9'"}
zipp = {version = "*", markers = "python_version < '3.9'"} zipp = {version = "*", markers = "python_version < '3.9'"}
pyzbar = "*" pyzbar = "*"
pdf2image = "*"
mysqlclient = "*" mysqlclient = "*"
setproctitle = "*"
[dev-packages] [dev-packages]
coveralls = "*" coveralls = "*"

154
Pipfile.lock generated
View File

@ -1,7 +1,7 @@
{ {
"_meta": { "_meta": {
"hash": { "hash": {
"sha256": "af0a0a5b996c11ad95266e98200640fd77648ec0f337ac3eb4e4b5ca1c714a0e" "sha256": "896665b8ff6d8a99af44b729c581033add1ba5cbd927723ef275649491c92a4f"
}, },
"pipfile-spec": 6, "pipfile-spec": 6,
"requires": {}, "requires": {},
@ -39,7 +39,7 @@
"sha256:3934b30ca1b9f292376d9db15b19446088d12ec58629bc3f0da28fd55fb633a1", "sha256:3934b30ca1b9f292376d9db15b19446088d12ec58629bc3f0da28fd55fb633a1",
"sha256:5a49ab92e3b7b71d96cd6bfcc4df14efefc9dfa96ea19045815914a6ab6b1fe2" "sha256:5a49ab92e3b7b71d96cd6bfcc4df14efefc9dfa96ea19045815914a6ab6b1fe2"
], ],
"markers": "python_full_version >= '3.6.0'", "markers": "python_version >= '3.6'",
"version": "==1.2.3" "version": "==1.2.3"
}, },
"asgiref": { "asgiref": {
@ -55,7 +55,7 @@
"sha256:2163e1640ddb52b7a8c80d0a67a08587e5d245cc9c553a74a847056bc2976b15", "sha256:2163e1640ddb52b7a8c80d0a67a08587e5d245cc9c553a74a847056bc2976b15",
"sha256:8ca1e4fcf50d07413d66d1a5e416e42cfdf5851c981d679a09851a6853383b3c" "sha256:8ca1e4fcf50d07413d66d1a5e416e42cfdf5851c981d679a09851a6853383b3c"
], ],
"markers": "python_full_version >= '3.6.0'", "markers": "python_version >= '3.6'",
"version": "==4.0.2" "version": "==4.0.2"
}, },
"attrs": { "attrs": {
@ -208,7 +208,7 @@
"sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845", "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845",
"sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f" "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f"
], ],
"markers": "python_full_version >= '3.6.0'", "markers": "python_version >= '3.6'",
"version": "==2.1.1" "version": "==2.1.1"
}, },
"click": { "click": {
@ -271,7 +271,7 @@
"sha256:d4ef6cc305394ed669d4d9eebf10d3a101059bdcf2669c366ec1d14e4fb227bd", "sha256:d4ef6cc305394ed669d4d9eebf10d3a101059bdcf2669c366ec1d14e4fb227bd",
"sha256:d9e69ae01f99abe6ad646947bba8941e896cb3aa805be2597a0400e0764b5818" "sha256:d9e69ae01f99abe6ad646947bba8941e896cb3aa805be2597a0400e0764b5818"
], ],
"markers": "python_full_version >= '3.6.0'", "markers": "python_version >= '3.6'",
"version": "==38.0.1" "version": "==38.0.1"
}, },
"daphne": { "daphne": {
@ -279,7 +279,7 @@
"sha256:76ffae916ba3aa66b46996c14fa713e46004788167a4873d647544e750e0e99f", "sha256:76ffae916ba3aa66b46996c14fa713e46004788167a4873d647544e750e0e99f",
"sha256:a9af943c79717bc52fe64a3c236ae5d3adccc8b5be19c881b442d2c3db233393" "sha256:a9af943c79717bc52fe64a3c236ae5d3adccc8b5be19c881b442d2c3db233393"
], ],
"markers": "python_full_version >= '3.6.0'", "markers": "python_version >= '3.6'",
"version": "==3.0.2" "version": "==3.0.2"
}, },
"dateparser": { "dateparser": {
@ -348,7 +348,7 @@
"django-q": { "django-q": {
"editable": true, "editable": true,
"git": "https://github.com/paperless-ngx/django-q.git", "git": "https://github.com/paperless-ngx/django-q.git",
"ref": "21ef9116f6386cf985ccd812dde4111efcfd1ba9" "ref": "8b5289d8caf36f67fb99448e76ead20d5b498c1b"
}, },
"djangorestframework": { "djangorestframework": {
"hashes": [ "hashes": [
@ -390,7 +390,7 @@
"sha256:70813c1135087a248a4d38cc0e1a0181ffab2188141a93eaf567940c3957ff06", "sha256:70813c1135087a248a4d38cc0e1a0181ffab2188141a93eaf567940c3957ff06",
"sha256:8ddd78563b633ca55346c8cd41ec0af27d3c79931828beffb46ce70a379e7442" "sha256:8ddd78563b633ca55346c8cd41ec0af27d3c79931828beffb46ce70a379e7442"
], ],
"markers": "python_full_version >= '3.6.0'", "markers": "python_version >= '3.6'",
"version": "==0.13.0" "version": "==0.13.0"
}, },
"hiredis": { "hiredis": {
@ -437,7 +437,7 @@
"sha256:f52010e0a44e3d8530437e7da38d11fb822acfb0d5b12e9cd5ba655509937ca0", "sha256:f52010e0a44e3d8530437e7da38d11fb822acfb0d5b12e9cd5ba655509937ca0",
"sha256:f8196f739092a78e4f6b1b2172679ed3343c39c61a3e9d722ce6fcf1dac2824a" "sha256:f8196f739092a78e4f6b1b2172679ed3343c39c61a3e9d722ce6fcf1dac2824a"
], ],
"markers": "python_full_version >= '3.6.0'", "markers": "python_version >= '3.6'",
"version": "==2.0.0" "version": "==2.0.0"
}, },
"httptools": { "httptools": {
@ -553,7 +553,7 @@
"sha256:4158fcecd13733f8be669be0683b96ebdbbd38d23559f54dca7205aea1bf1e35", "sha256:4158fcecd13733f8be669be0683b96ebdbbd38d23559f54dca7205aea1bf1e35",
"sha256:f21f109b3c7ff9d95f8387f752d0d9c34a02aa2f7060c2135f465da0e5160ff6" "sha256:f21f109b3c7ff9d95f8387f752d0d9c34a02aa2f7060c2135f465da0e5160ff6"
], ],
"markers": "python_full_version >= '3.6.0'", "markers": "python_version >= '3.6'",
"version": "==1.1.0" "version": "==1.1.0"
}, },
"langdetect": { "langdetect": {
@ -712,37 +712,57 @@
}, },
"numpy": { "numpy": {
"hashes": [ "hashes": [
"sha256:07a8c89a04997625236c5ecb7afe35a02af3896c8aa01890a849913a2309c676",
"sha256:08d9b008d0156c70dc392bb3ab3abb6e7a711383c3247b410b39962263576cd4",
"sha256:17e5226674f6ea79e14e3b91bfbc153fdf3ac13f5cc54ee7bc8fdbe820a32da0", "sha256:17e5226674f6ea79e14e3b91bfbc153fdf3ac13f5cc54ee7bc8fdbe820a32da0",
"sha256:201b4d0552831f7250a08d3b38de0d989d6f6e4658b709a02a73c524ccc6ffce",
"sha256:2bd879d3ca4b6f39b7770829f73278b7c5e248c91d538aab1e506c628353e47f", "sha256:2bd879d3ca4b6f39b7770829f73278b7c5e248c91d538aab1e506c628353e47f",
"sha256:2c10a93606e0b4b95c9b04b77dc349b398fdfbda382d2a39ba5a822f669a0123",
"sha256:3ca688e1b9b95d80250bca34b11a05e389b1420d00e87a0d12dc45f131f704a1",
"sha256:48a3aecd3b997bf452a2dedb11f4e79bc5bfd21a1d4cc760e703c31d57c84b3e",
"sha256:4f41f5bf20d9a521f8cab3a34557cd77b6f205ab2116651f12959714494268b0", "sha256:4f41f5bf20d9a521f8cab3a34557cd77b6f205ab2116651f12959714494268b0",
"sha256:5593f67e66dea4e237f5af998d31a43e447786b2154ba1ad833676c788f37cde", "sha256:5593f67e66dea4e237f5af998d31a43e447786b2154ba1ad833676c788f37cde",
"sha256:568dfd16224abddafb1cbcce2ff14f522abe037268514dd7e42c6776a1c3f8e5",
"sha256:5bfb1bb598e8229c2d5d48db1860bcf4311337864ea3efdbe1171fb0c5da515d",
"sha256:5e28cd64624dc2354a349152599e55308eb6ca95a13ce6a7d5679ebff2962913", "sha256:5e28cd64624dc2354a349152599e55308eb6ca95a13ce6a7d5679ebff2962913",
"sha256:633679a472934b1c20a12ed0c9a6c9eb167fbb4cb89031939bfd03dd9dbc62b8", "sha256:633679a472934b1c20a12ed0c9a6c9eb167fbb4cb89031939bfd03dd9dbc62b8",
"sha256:639b54cdf6aa4f82fe37ebf70401bbb74b8508fddcf4797f9fe59615b8c5813a",
"sha256:806970e69106556d1dd200e26647e9bee5e2b3f1814f9da104a943e8d548ca38", "sha256:806970e69106556d1dd200e26647e9bee5e2b3f1814f9da104a943e8d548ca38",
"sha256:806cc25d5c43e240db709875e947076b2826f47c2c340a5a2f36da5bb10c58d6", "sha256:806cc25d5c43e240db709875e947076b2826f47c2c340a5a2f36da5bb10c58d6",
"sha256:8247f01c4721479e482cc2f9f7d973f3f47810cbc8c65e38fd1bbd3141cc9842", "sha256:8247f01c4721479e482cc2f9f7d973f3f47810cbc8c65e38fd1bbd3141cc9842",
"sha256:8251ed96f38b47b4295b1ae51631de7ffa8260b5b087808ef09a39a9d66c97ab",
"sha256:8ebf7e194b89bc66b78475bd3624d92980fca4e5bb86dda08d677d786fefc414", "sha256:8ebf7e194b89bc66b78475bd3624d92980fca4e5bb86dda08d677d786fefc414",
"sha256:8ecb818231afe5f0f568c81f12ce50f2b828ff2b27487520d85eb44c71313b9e", "sha256:8ecb818231afe5f0f568c81f12ce50f2b828ff2b27487520d85eb44c71313b9e",
"sha256:8f9d84a24889ebb4c641a9b99e54adb8cab50972f0166a3abc14c3b93163f074", "sha256:8f9d84a24889ebb4c641a9b99e54adb8cab50972f0166a3abc14c3b93163f074",
"sha256:909c56c4d4341ec8315291a105169d8aae732cfb4c250fbc375a1efb7a844f8f", "sha256:909c56c4d4341ec8315291a105169d8aae732cfb4c250fbc375a1efb7a844f8f",
"sha256:92bfa69cfbdf7dfc3040978ad09a48091143cffb778ec3b03fa170c494118d75",
"sha256:97098b95aa4e418529099c26558eeb8486e66bd1e53a6b606d684d0c3616b168",
"sha256:9b83d48e464f393d46e8dd8171687394d39bc5abfe2978896b77dc2604e8635d", "sha256:9b83d48e464f393d46e8dd8171687394d39bc5abfe2978896b77dc2604e8635d",
"sha256:a3bae1a2ed00e90b3ba5f7bd0a7c7999b55d609e0c54ceb2b076a25e345fa9f4",
"sha256:ac987b35df8c2a2eab495ee206658117e9ce867acf3ccb376a19e83070e69418", "sha256:ac987b35df8c2a2eab495ee206658117e9ce867acf3ccb376a19e83070e69418",
"sha256:b78d00e48261fbbd04aa0d7427cf78d18401ee0abd89c7559bbf422e5b1c7d01", "sha256:b78d00e48261fbbd04aa0d7427cf78d18401ee0abd89c7559bbf422e5b1c7d01",
"sha256:b8b97a8a87cadcd3f94659b4ef6ec056261fa1e1c3317f4193ac231d4df70215", "sha256:b8b97a8a87cadcd3f94659b4ef6ec056261fa1e1c3317f4193ac231d4df70215",
"sha256:bd5b7ccae24e3d8501ee5563e82febc1771e73bd268eef82a1e8d2b4d556ae66", "sha256:bd5b7ccae24e3d8501ee5563e82febc1771e73bd268eef82a1e8d2b4d556ae66",
"sha256:bdc02c0235b261925102b1bd586579b7158e9d0d07ecb61148a1799214a4afd5", "sha256:bdc02c0235b261925102b1bd586579b7158e9d0d07ecb61148a1799214a4afd5",
"sha256:be6b350dfbc7f708d9d853663772a9310783ea58f6035eec649fb9c4371b5389", "sha256:be6b350dfbc7f708d9d853663772a9310783ea58f6035eec649fb9c4371b5389",
"sha256:c34ea7e9d13a70bf2ab64a2532fe149a9aced424cd05a2c4ba662fd989e3e45f",
"sha256:c403c81bb8ffb1c993d0165a11493fd4bf1353d258f6997b3ee288b0a48fce77", "sha256:c403c81bb8ffb1c993d0165a11493fd4bf1353d258f6997b3ee288b0a48fce77",
"sha256:cf8c6aed12a935abf2e290860af8e77b26a042eb7f2582ff83dc7ed5f963340c", "sha256:cf8c6aed12a935abf2e290860af8e77b26a042eb7f2582ff83dc7ed5f963340c",
"sha256:d98addfd3c8728ee8b2c49126f3c44c703e2b005d4a95998e2167af176a9e722", "sha256:d98addfd3c8728ee8b2c49126f3c44c703e2b005d4a95998e2167af176a9e722",
"sha256:dbc7601a3b7472d559dc7b933b18b4b66f9aa7452c120e87dfb33d02008c8a18",
"sha256:dc76bca1ca98f4b122114435f83f1fcf3c0fe48e4e6f660e07996abf2f53903c", "sha256:dc76bca1ca98f4b122114435f83f1fcf3c0fe48e4e6f660e07996abf2f53903c",
"sha256:dec198619b7dbd6db58603cd256e092bcadef22a796f778bf87f8592b468441d", "sha256:dec198619b7dbd6db58603cd256e092bcadef22a796f778bf87f8592b468441d",
"sha256:df28dda02c9328e122661f399f7655cdcbcf22ea42daa3650a26bce08a187450", "sha256:df28dda02c9328e122661f399f7655cdcbcf22ea42daa3650a26bce08a187450",
"sha256:e603ca1fb47b913942f3e660a15e55a9ebca906857edfea476ae5f0fe9b457d5", "sha256:e603ca1fb47b913942f3e660a15e55a9ebca906857edfea476ae5f0fe9b457d5",
"sha256:ecfdd68d334a6b97472ed032b5b37a30d8217c097acfff15e8452c710e775524" "sha256:e7927a589df200c5e23c57970bafbd0cd322459aa7b1ff73b7c2e84d6e3eae62",
"sha256:ecfdd68d334a6b97472ed032b5b37a30d8217c097acfff15e8452c710e775524",
"sha256:f8c1f39caad2c896bc0018f699882b345b2a63708008be29b1f355ebf6f933fe",
"sha256:f950f8845b480cffe522913d35567e29dd381b0dc7e4ce6a4a9f9156417d2430",
"sha256:fade0d4f4d292b6f39951b6836d7a3c7ef5b2347f3c420cd9820a1d90d794802",
"sha256:fdf3c08bce27132395d3c3ba1503cac12e17282358cb4bddc25cc46b0aca07aa"
], ],
"markers": "python_version >= '3.8'", "index": "pypi",
"version": "==1.23.2" "version": "==1.22.3"
}, },
"ocrmypdf": { "ocrmypdf": {
"hashes": [ "hashes": [
@ -757,7 +777,7 @@
"sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb", "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb",
"sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522" "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"
], ],
"markers": "python_full_version >= '3.6.0'", "markers": "python_version >= '3.6'",
"version": "==21.3" "version": "==21.3"
}, },
"pathvalidate": { "pathvalidate": {
@ -768,14 +788,6 @@
"index": "pypi", "index": "pypi",
"version": "==2.5.2" "version": "==2.5.2"
}, },
"pdf2image": {
"hashes": [
"sha256:84f79f2b8fad943e36323ea4e937fcb05f26ded0caa0a01181df66049e42fb65",
"sha256:d58ed94d978a70c73c2bb7fdf8acbaf2a7089c29ff8141be5f45433c0c4293bb"
],
"index": "pypi",
"version": "==1.16.0"
},
"pdfminer.six": { "pdfminer.six": {
"hashes": [ "hashes": [
"sha256:5a64c924410ac48501d6060b21638bf401db69f5b1bd57207df7fbc070ac8ae2", "sha256:5a64c924410ac48501d6060b21638bf401db69f5b1bd57207df7fbc070ac8ae2",
@ -894,7 +906,7 @@
"sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159", "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159",
"sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3" "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"
], ],
"markers": "python_full_version >= '3.6.0'", "markers": "python_version >= '3.6'",
"version": "==1.0.0" "version": "==1.0.0"
}, },
"portalocker": { "portalocker": {
@ -1035,6 +1047,7 @@
}, },
"pyyaml": { "pyyaml": {
"hashes": [ "hashes": [
"sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf",
"sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293", "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293",
"sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b", "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b",
"sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57", "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57",
@ -1046,26 +1059,32 @@
"sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287", "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287",
"sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513", "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513",
"sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0", "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0",
"sha256:432557aa2c09802be39460360ddffd48156e30721f5e8d917f01d31694216782",
"sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0", "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0",
"sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92", "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92",
"sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f", "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f",
"sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2", "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2",
"sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc", "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc",
"sha256:81957921f441d50af23654aa6c5e5eaf9b06aba7f0a19c18a538dc7ef291c5a1",
"sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c", "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c",
"sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86", "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86",
"sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4", "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4",
"sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c", "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c",
"sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34", "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34",
"sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b", "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b",
"sha256:afa17f5bc4d1b10afd4466fd3a44dc0e245382deca5b3c353d8b757f9e3ecb8d",
"sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c", "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c",
"sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb", "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb",
"sha256:bfaef573a63ba8923503d27530362590ff4f576c626d86a9fed95822a8255fd7",
"sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737", "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737",
"sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3", "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3",
"sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d", "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d",
"sha256:d4b0ba9512519522b118090257be113b9468d804b19d63c71dbcf4a48fa32358",
"sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53", "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53",
"sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78", "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78",
"sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803", "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803",
"sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a", "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a",
"sha256:dbad0e9d368bb989f4515da330b88a057617d16b6a8245084f1b05400f24609f",
"sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174", "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174",
"sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5" "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"
], ],
@ -1165,7 +1184,7 @@
"sha256:fbc88d3ba402b5d041d204ec2449c4078898f89c4a6e6f0ed1c1a510ef1e221d", "sha256:fbc88d3ba402b5d041d204ec2449c4078898f89c4a6e6f0ed1c1a510ef1e221d",
"sha256:fbd3fe37353c62fd0eb19fb76f78aa693716262bcd5f9c14bb9e5aca4b3f0dc4" "sha256:fbd3fe37353c62fd0eb19fb76f78aa693716262bcd5f9c14bb9e5aca4b3f0dc4"
], ],
"markers": "python_full_version >= '3.6.0'", "markers": "python_version >= '3.6'",
"version": "==2022.3.2" "version": "==2022.3.2"
}, },
"reportlab": { "reportlab": {
@ -1278,6 +1297,72 @@
], ],
"version": "==21.1.0" "version": "==21.1.0"
}, },
"setproctitle": {
"hashes": [
"sha256:1c5d5dad7c28bdd1ec4187d818e43796f58a845aa892bb4481587010dc4d362b",
"sha256:1c8d9650154afaa86a44ff195b7b10d683c73509d085339d174e394a22cccbb9",
"sha256:1f0cde41857a644b7353a0060b5f94f7ba7cf593ebde5a1094da1be581ac9a31",
"sha256:1f29b75e86260b0ab59adb12661ef9f113d2f93a59951373eb6d68a852b13e83",
"sha256:1fa1a0fbee72b47dc339c87c890d3c03a72ea65c061ade3204f285582f2da30f",
"sha256:1ff863a20d1ff6ba2c24e22436a3daa3cd80be1dfb26891aae73f61b54b04aca",
"sha256:265ecbe2c6eafe82e104f994ddd7c811520acdd0647b73f65c24f51374cf9494",
"sha256:288943dec88e178bb2fd868adf491197cc0fc8b6810416b1c6775e686bab87fe",
"sha256:2e3ac25bfc4a0f29d2409650c7532d5ddfdbf29f16f8a256fc31c47d0dc05172",
"sha256:2fbd8187948284293f43533c150cd69a0e4192c83c377da837dbcd29f6b83084",
"sha256:4058564195b975ddc3f0462375c533cce310ccdd41b80ac9aed641c296c3eff4",
"sha256:4749a2b0c9ac52f864d13cee94546606f92b981b50e46226f7f830a56a9dc8e1",
"sha256:4d8938249a7cea45ab7e1e48b77685d0f2bab1ebfa9dde23e94ab97968996a7c",
"sha256:5194b4969f82ea842a4f6af2f82cd16ebdc3f1771fb2771796e6add9835c1973",
"sha256:55ce1e9925ce1765865442ede9dca0ba9bde10593fcd570b1f0fa25d3ec6b31c",
"sha256:589be87172b238f839e19f146b9ea47c71e413e951ef0dc6db4218ddacf3c202",
"sha256:5b932c3041aa924163f4aab970c2f0e6b4d9d773f4d50326e0ea1cd69240e5c5",
"sha256:5fb4f769c02f63fac90989711a3fee83919f47ae9afd4758ced5d86596318c65",
"sha256:630f6fe5e24a619ccf970c78e084319ee8be5be253ecc9b5b216b0f474f5ef18",
"sha256:65d884e22037b23fa25b2baf1a3316602ed5c5971eb3e9d771a38c3a69ce6e13",
"sha256:6c877691b90026670e5a70adfbcc735460a9f4c274d35ec5e8a43ce3f8443005",
"sha256:710e16fa3bade3b026907e4a5e841124983620046166f355bbb84be364bf2a02",
"sha256:7a55fe05f15c10e8c705038777656fe45e3bd676d49ad9ac8370b75c66dd7cd7",
"sha256:7aa0aac1711fadffc1d51e9d00a3bea61f68443d6ac0241a224e4d622489d665",
"sha256:7f0bed90a216ef28b9d227d8d73e28a8c9b88c0f48a082d13ab3fa83c581488f",
"sha256:7f2719a398e1a2c01c2a63bf30377a34d0b6ef61946ab9cf4d550733af8f1ef1",
"sha256:7fe9df7aeb8c64db6c34fc3b13271a363475d77bc157d3f00275a53910cb1989",
"sha256:8ff3c8cb26afaed25e8bca7b9dd0c1e36de71f35a3a0706b5c0d5172587a3827",
"sha256:9124bedd8006b0e04d4e8a71a0945da9b67e7a4ab88fdad7b1440dc5b6122c42",
"sha256:92c626edc66169a1b09e9541b9c0c9f10488447d8a2b1d87c8f0672e771bc927",
"sha256:a149a5f7f2c5a065d4e63cb0d7a4b6d3b66e6e80f12e3f8827c4f63974cbf122",
"sha256:a47d97a75fd2d10c37410b180f67a5835cb1d8fdea2648fd7f359d4277f180b9",
"sha256:a499fff50387c1520c085a07578a000123f519e5f3eee61dd68e1d301659651f",
"sha256:ab45146c71ca6592c9cc8b354a2cc9cc4843c33efcbe1d245d7d37ce9696552d",
"sha256:b2c9cb2705fc84cb8798f1ba74194f4c080aaef19d9dae843591c09b97678e98",
"sha256:b34baef93bfb20a8ecb930e395ccd2ae3268050d8cf4fe187de5e2bd806fd796",
"sha256:b617f12c9be61e8f4b2857be4a4319754756845dbbbd9c3718f468bbb1e17bcb",
"sha256:b9fb97907c830d260fa0658ed58afd48a86b2b88aac521135c352ff7fd3477fd",
"sha256:bae283e85fc084b18ffeb92e061ff7ac5af9e183c9d1345c93e178c3e5069cbe",
"sha256:c2c46200656280a064073447ebd363937562debef329482fd7e570c8d498f806",
"sha256:c8a09d570b39517de10ee5b718730e171251ce63bbb890c430c725c8c53d4484",
"sha256:c91b9bc8985d00239f7dc08a49927a7ca1ca8a6af2c3890feec3ed9665b6f91e",
"sha256:dad42e676c5261eb50fdb16bdf3e2771cf8f99a79ef69ba88729aeb3472d8575",
"sha256:de3a540cd1817ede31f530d20e6a4935bbc1b145fd8f8cf393903b1e02f1ae76",
"sha256:e00c9d5c541a2713ba0e657e0303bf96ddddc412ef4761676adc35df35d7c246",
"sha256:e1aafc91cbdacc9e5fe712c52077369168e6b6c346f3a9d51bf600b53eae56bb",
"sha256:e425be62524dc0c593985da794ee73eb8a17abb10fe692ee43bb39e201d7a099",
"sha256:e43f315c68aa61cbdef522a2272c5a5b9b8fd03c301d3167b5e1343ef50c676c",
"sha256:e49ae693306d7624015f31cb3e82708916759d592c2e5f72a35c8f4cc8aef258",
"sha256:e5c50e164cd2459bc5137c15288a9ef57160fd5cbf293265ea3c45efe7870865",
"sha256:e8579a43eafd246e285eb3a5b939e7158073d5087aacdd2308f23200eac2458b",
"sha256:e85e50b9c67854f89635a86247412f3ad66b132a4d8534ac017547197c88f27d",
"sha256:f0452282258dfcc01697026a8841258dd2057c4438b43914b611bccbcd048f10",
"sha256:f4bfc89bd33ebb8e4c0e9846a09b1f5a4a86f5cb7a317e75cc42fee1131b4f4f",
"sha256:fa2f50678f04fda7a75d0fe5dd02bbdd3b13cbe6ed4cf626e4472a7ccf47ae94",
"sha256:faec934cfe5fd6ac1151c02e67156c3f526e82f96b24d550b5d51efa4a5527c6",
"sha256:fcd3cf4286a60fdc95451d8d14e0389a6b4f5cebe02c7f2609325eb016535963",
"sha256:fe8a988c7220c002c45347430993830666e55bc350179d91fcee0feafe64e1d4",
"sha256:fed18e44711c5af4b681c2b3b18f85e6f0f1b2370a28854c645d636d5305ccd8",
"sha256:ffc61a388a5834a97953d6444a2888c24a05f2e333f9ed49f977a87bb1ad4761"
],
"index": "pypi",
"version": "==1.3.2"
},
"setuptools": { "setuptools": {
"hashes": [ "hashes": [
"sha256:2e24e0bec025f035a2e72cdd1961119f557d78ad331bb00ff82efb2ab8da8e82", "sha256:2e24e0bec025f035a2e72cdd1961119f557d78ad331bb00ff82efb2ab8da8e82",
@ -1315,7 +1400,7 @@
"sha256:8b99adda265feb6773280df41eece7b2e6561b772d21ffd52e372f999024907b", "sha256:8b99adda265feb6773280df41eece7b2e6561b772d21ffd52e372f999024907b",
"sha256:a335baacfaa4400ae1f0d8e3a58d6674d2f8828e3716bb2802c44955ad391380" "sha256:a335baacfaa4400ae1f0d8e3a58d6674d2f8828e3716bb2802c44955ad391380"
], ],
"markers": "python_full_version >= '3.6.0'", "markers": "python_version >= '3.6'",
"version": "==3.1.0" "version": "==3.1.0"
}, },
"tika": { "tika": {
@ -1349,7 +1434,7 @@
"sha256:2e4582b70f04b2345908254684a984206c0d9b50e3074a24a4c55aba21d24d01", "sha256:2e4582b70f04b2345908254684a984206c0d9b50e3074a24a4c55aba21d24d01",
"sha256:41223af4a9d5726e645a8ee82480f413e5e300dd257db94bc38ae12ea48fb2e5" "sha256:41223af4a9d5726e645a8ee82480f413e5e300dd257db94bc38ae12ea48fb2e5"
], ],
"markers": "python_full_version >= '3.6.0'", "markers": "python_version >= '3.6'",
"version": "==22.2.1" "version": "==22.2.1"
}, },
"typing-extensions": { "typing-extensions": {
@ -1365,7 +1450,7 @@
"sha256:21f4f0d7241572efa7f7a4fdabb052e61b55dc48274e6842697ccdf5253e5451", "sha256:21f4f0d7241572efa7f7a4fdabb052e61b55dc48274e6842697ccdf5253e5451",
"sha256:c3119520447d68ef3eb8187a55a4f44fa455f30eb1b4238fa5691ba094f2b05b" "sha256:c3119520447d68ef3eb8187a55a4f44fa455f30eb1b4238fa5691ba094f2b05b"
], ],
"markers": "python_full_version >= '3.6.0'", "markers": "python_version >= '3.6'",
"version": "==2022.2" "version": "==2022.2"
}, },
"tzlocal": { "tzlocal": {
@ -1373,7 +1458,7 @@
"sha256:89885494684c929d9191c57aa27502afc87a579be5cdd3225c77c463ea043745", "sha256:89885494684c929d9191c57aa27502afc87a579be5cdd3225c77c463ea043745",
"sha256:ee5842fa3a795f023514ac2d801c4a81d1743bbe642e3940143326b3a00addd7" "sha256:ee5842fa3a795f023514ac2d801c4a81d1743bbe642e3940143326b3a00addd7"
], ],
"markers": "python_full_version >= '3.6.0'", "markers": "python_version >= '3.6'",
"version": "==4.2" "version": "==4.2"
}, },
"urllib3": { "urllib3": {
@ -1757,7 +1842,7 @@
"sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845", "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845",
"sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f" "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f"
], ],
"markers": "python_full_version >= '3.6.0'", "markers": "python_version >= '3.6'",
"version": "==2.1.1" "version": "==2.1.1"
}, },
"click": { "click": {
@ -2036,7 +2121,7 @@
"sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb", "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb",
"sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522" "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"
], ],
"markers": "python_full_version >= '3.6.0'", "markers": "python_version >= '3.6'",
"version": "==21.3" "version": "==21.3"
}, },
"pathspec": { "pathspec": {
@ -2060,7 +2145,7 @@
"sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159", "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159",
"sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3" "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"
], ],
"markers": "python_full_version >= '3.6.0'", "markers": "python_version >= '3.6'",
"version": "==1.0.0" "version": "==1.0.0"
}, },
"pre-commit": { "pre-commit": {
@ -2175,6 +2260,7 @@
}, },
"pyyaml": { "pyyaml": {
"hashes": [ "hashes": [
"sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf",
"sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293", "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293",
"sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b", "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b",
"sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57", "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57",
@ -2186,26 +2272,32 @@
"sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287", "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287",
"sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513", "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513",
"sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0", "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0",
"sha256:432557aa2c09802be39460360ddffd48156e30721f5e8d917f01d31694216782",
"sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0", "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0",
"sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92", "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92",
"sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f", "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f",
"sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2", "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2",
"sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc", "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc",
"sha256:81957921f441d50af23654aa6c5e5eaf9b06aba7f0a19c18a538dc7ef291c5a1",
"sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c", "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c",
"sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86", "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86",
"sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4", "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4",
"sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c", "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c",
"sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34", "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34",
"sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b", "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b",
"sha256:afa17f5bc4d1b10afd4466fd3a44dc0e245382deca5b3c353d8b757f9e3ecb8d",
"sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c", "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c",
"sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb", "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb",
"sha256:bfaef573a63ba8923503d27530362590ff4f576c626d86a9fed95822a8255fd7",
"sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737", "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737",
"sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3", "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3",
"sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d", "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d",
"sha256:d4b0ba9512519522b118090257be113b9468d804b19d63c71dbcf4a48fa32358",
"sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53", "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53",
"sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78", "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78",
"sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803", "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803",
"sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a", "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a",
"sha256:dbad0e9d368bb989f4515da330b88a057617d16b6a8245084f1b05400f24609f",
"sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174", "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174",
"sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5" "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"
], ],

View File

@ -36,3 +36,7 @@
# The default language to use for OCR. Set this to the language most of your # The default language to use for OCR. Set this to the language most of your
# documents are written in. # documents are written in.
#PAPERLESS_OCR_LANGUAGE=eng #PAPERLESS_OCR_LANGUAGE=eng
# Set if accessing paperless via a domain subpath e.g. https://domain.com/PATHPREFIX and using a reverse-proxy like traefik or nginx
#PAPERLESS_FORCE_SCRIPT_NAME=/PATHPREFIX
#PAPERLESS_STATIC_URL=/PATHPREFIX/static/ # trailing slash required

View File

@ -50,6 +50,7 @@ map_folders() {
# Export these so they can be used in docker-prepare.sh # Export these so they can be used in docker-prepare.sh
export DATA_DIR="${PAPERLESS_DATA_DIR:-/usr/src/paperless/data}" export DATA_DIR="${PAPERLESS_DATA_DIR:-/usr/src/paperless/data}"
export MEDIA_ROOT_DIR="${PAPERLESS_MEDIA_ROOT:-/usr/src/paperless/media}" export MEDIA_ROOT_DIR="${PAPERLESS_MEDIA_ROOT:-/usr/src/paperless/media}"
export CONSUME_DIR="${PAPERLESS_CONSUMPTION_DIR:-/usr/src/paperless/consume}"
} }
initialize() { initialize() {
@ -77,7 +78,11 @@ initialize() {
local export_dir="/usr/src/paperless/export" local export_dir="/usr/src/paperless/export"
for dir in "${export_dir}" "${DATA_DIR}" "${DATA_DIR}/index" "${MEDIA_ROOT_DIR}" "${MEDIA_ROOT_DIR}/documents" "${MEDIA_ROOT_DIR}/documents/originals" "${MEDIA_ROOT_DIR}/documents/thumbnails"; do for dir in \
"${export_dir}" \
"${DATA_DIR}" "${DATA_DIR}/index" \
"${MEDIA_ROOT_DIR}" "${MEDIA_ROOT_DIR}/documents" "${MEDIA_ROOT_DIR}/documents/originals" "${MEDIA_ROOT_DIR}/documents/thumbnails" \
"${CONSUME_DIR}"; do
if [[ ! -d "${dir}" ]]; then if [[ ! -d "${dir}" ]]; then
echo "Creating directory ${dir}" echo "Creating directory ${dir}"
mkdir "${dir}" mkdir "${dir}"
@ -91,7 +96,11 @@ initialize() {
set +e set +e
echo "Adjusting permissions of paperless files. This may take a while." echo "Adjusting permissions of paperless files. This may take a while."
chown -R paperless:paperless ${tmp_dir} chown -R paperless:paperless ${tmp_dir}
for dir in "${export_dir}" "${DATA_DIR}" "${MEDIA_ROOT_DIR}"; do for dir in \
"${export_dir}" \
"${DATA_DIR}" \
"${MEDIA_ROOT_DIR}" \
"${CONSUME_DIR}"; do
find "${dir}" -not \( -user paperless -and -group paperless \) -exec chown paperless:paperless {} + find "${dir}" -not \( -user paperless -and -group paperless \) -exec chown paperless:paperless {} +
done done
set -e set -e

View File

@ -12,4 +12,4 @@ if [ "$(id -u)" == "$(id -u paperless)" ]; then
) )
fi fi
/usr/local/bin/supervisord -c /etc/supervisord.conf "${rootless_args[@]}" exec /usr/local/bin/supervisord -c /etc/supervisord.conf "${rootless_args[@]}"

View File

@ -27,6 +27,12 @@ PAPERLESS_REDIS=<url>
This is required for processing scheduled tasks such as email fetching, index This is required for processing scheduled tasks such as email fetching, index
optimization and for training the automatic document matcher. optimization and for training the automatic document matcher.
* If your Redis server needs login credentials PAPERLESS_REDIS = ``redis://<username>:<password>@<host>:<port>``
* With the requirepass option PAPERLESS_REDIS = ``redis://:<password>@<host>:<port>``
`More information on securing your Redis Instance <https://redis.io/docs/getting-started/#securing-redis>`_.
Defaults to redis://localhost:6379. Defaults to redis://localhost:6379.
PAPERLESS_DBENGINE=<engine_name> PAPERLESS_DBENGINE=<engine_name>
@ -215,9 +221,16 @@ PAPERLESS_FORCE_SCRIPT_NAME=<path>
PAPERLESS_STATIC_URL=<path> PAPERLESS_STATIC_URL=<path>
Override the STATIC_URL here. Unless you're hosting Paperless off a Override the STATIC_URL here. Unless you're hosting Paperless off a
subdomain like /paperless/, you probably don't need to change this. subdomain like /paperless/, you probably don't need to change this.
If you do change it, be sure to include the trailing slash.
Defaults to "/static/". Defaults to "/static/".
.. note::
When hosting paperless behind a reverse proxy like Traefik or Nginx at a subpath e.g.
example.com/paperlessngx you will also need to set ``PAPERLESS_FORCE_SCRIPT_NAME``
(see above).
PAPERLESS_AUTO_LOGIN_USERNAME=<username> PAPERLESS_AUTO_LOGIN_USERNAME=<username>
Specify a username here so that paperless will automatically perform login Specify a username here so that paperless will automatically perform login
with the selected user. with the selected user.
@ -805,10 +818,10 @@ the program doesn't automatically execute it (ie. the program isn't in your
$PATH), then you'll need to specify the literal path for that program. $PATH), then you'll need to specify the literal path for that program.
PAPERLESS_CONVERT_BINARY=<path> PAPERLESS_CONVERT_BINARY=<path>
Defaults to "/usr/bin/convert". Defaults to "convert".
PAPERLESS_GS_BINARY=<path> PAPERLESS_GS_BINARY=<path>
Defaults to "/usr/bin/gs". Defaults to "gs".
.. _configuration-docker: .. _configuration-docker:

View File

@ -1,142 +1,8 @@
.. _scanners: .. _scanners:
*********************** *******************
Scanner recommendations Scanners & Software
*********************** *******************
As Paperless operates by watching a folder for new files, doesn't care what Paperless-ngx is compatible with many different scanners and scanning tools. A user-maintained list of scanners and other software is available on `the wiki <https://github.com/paperless-ngx/paperless-ngx/wiki/Scanner-&-Software-Recommendations>`_.
scanner you use, but sometimes finding a scanner that will write to an FTP,
NFS, or SMB server can be difficult. This page is here to help you find one
that works right for you based on recommendations from other Paperless users.
Physical scanners
=================
+---------+-------------------+-----+------+-----+----------+------+----------+----------------+
| Brand | Model | Supports | Recommended By |
+---------+-------------------+-----+------+-----+----------+------+----------+----------------+
| | | FTP | SFTP | NFS | SMB | SMTP | API [1]_ | |
+=========+===================+=====+======+=====+==========+======+==========+================+
| Brother | `ADS-1700W`_ | yes | yes | | yes | yes | |`holzhannes`_ |
+---------+-------------------+-----+------+-----+----------+------+----------+----------------+
| Brother | `ADS-1600W`_ | yes | | | yes | yes | |`holzhannes`_ |
+---------+-------------------+-----+------+-----+----------+------+----------+----------------+
| Brother | `ADS-1500W`_ | yes | | | yes | yes | |`danielquinn`_ |
+---------+-------------------+-----+------+-----+----------+------+----------+----------------+
| Brother | `ADS-1100W`_ | yes | | | | | |`ytzelf`_ |
+---------+-------------------+-----+------+-----+----------+------+----------+----------------+
| Brother | `ADS-2800W`_ | yes | yes | | yes | yes | |`philpagel`_ |
+---------+-------------------+-----+------+-----+----------+------+----------+----------------+
| Brother | `MFC-J6930DW`_ | yes | | | | | |`ayounggun`_ |
+---------+-------------------+-----+------+-----+----------+------+----------+----------------+
| Brother | `MFC-L5850DW`_ | yes | | | | yes | |`holzhannes`_ |
+---------+-------------------+-----+------+-----+----------+------+----------+----------------+
| Brother | `MFC-L2750DW`_ | yes | | | yes | yes | |`muued`_ |
+---------+-------------------+-----+------+-----+----------+------+----------+----------------+
| Brother | `MFC-J5910DW`_ | yes | | | | | |`bmsleight`_ |
+---------+-------------------+-----+------+-----+----------+------+----------+----------------+
| Brother | `MFC-8950DW`_ | yes | | | yes | yes | |`philpagel`_ |
+---------+-------------------+-----+------+-----+----------+------+----------+----------------+
| Brother | `MFC-9142CDN`_ | yes | | | yes | | |`REOLDEV`_ |
+---------+-------------------+-----+------+-----+----------+------+----------+----------------+
| Canon | `Maxify MB 5350`_ | | | | yes [2]_ | yes | |`eingemaischt`_ |
+---------+-------------------+-----+------+-----+----------+------+----------+----------------+
| Fujitsu | `ix500`_ | yes | | | yes | | |`eonist`_ |
+---------+-------------------+-----+------+-----+----------+------+----------+----------------+
| Epson | `ES-580W`_ | yes | | | yes | yes | |`fignew`_ |
+---------+-------------------+-----+------+-----+----------+------+----------+----------------+
| Epson | `WF-7710DWF`_ | yes | | | yes | | |`Skylinar`_ |
+---------+-------------------+-----+------+-----+----------+------+----------+----------------+
| Fujitsu | `S1300i`_ | yes | | | yes | | |`jonaswinkler`_ |
+---------+-------------------+-----+------+-----+----------+------+----------+----------------+
| Doxie | `Q2`_ | | | | | | yes |`Unkn0wnCat`_ |
+---------+-------------------+-----+------+-----+----------+------+----------+----------------+
.. _MFC-L5850DW: https://www.brother-usa.com/products/mfcl5850dw
.. _MFC-L2750DW: https://www.brother.de/drucker/laserdrucker/mfc-l2750dw
.. _ADS-1700W: https://www.brother-usa.com/products/ads1700w
.. _ADS-1600W: https://www.brother-usa.com/products/ads1600w
.. _ADS-1500W: https://www.brother.ca/en/p/ads1500w
.. _ADS-1100W: https://support.brother.com/g/b/downloadtop.aspx?c=fr&lang=fr&prod=ads1100w_eu_as_cn
.. _ADS-2800W: https://www.brother-usa.com/products/ads2800w
.. _Maxify MB 5350: https://www.canon.de/printers/inkjet/maxify/maxify_mb5350/specification.html
.. _MFC-J6930DW: https://www.brother.ca/en/p/MFCJ6930DW
.. _MFC-J5910DW: https://www.brother.co.uk/printers/inkjet-printers/mfcj5910dw
.. _MFC-8950DW: https://www.brother-usa.com/products/mfc8950dw
.. _MFC-9142CDN: https://www.brother.co.uk/printers/laser-printers/mfc9140cdn
.. _ES-580W: https://epson.com/Support/Scanners/ES-Series/Epson-WorkForce-ES-580W/s/SPT_B11B258201
.. _WF-7710DWF: https://www.epson.de/en/products/printers/inkjet-printers/for-home/workforce-wf-7710dwf
.. _ix500: http://www.fujitsu.com/us/products/computing/peripheral/scanners/scansnap/ix500/
.. _S1300i: https://www.fujitsu.com/global/products/computing/peripheral/scanners/soho/s1300i/
.. _Q2: https://www.getdoxie.com/product/doxie-q/
.. _ayounggun: https://github.com/ayounggun
.. _bmsleight: https://github.com/bmsleight
.. _danielquinn: https://github.com/danielquinn
.. _eonist: https://github.com/eonist
.. _fignew: https://github.com/fignew
.. _holzhannes: https://github.com/holzhannes
.. _jonaswinkler: https://github.com/jonaswinkler
.. _REOLDEV: https://github.com/REOLDEV
.. _Skylinar: https://github.com/Skylinar
.. _ytzelf: https://github.com/ytzelf
.. _Unkn0wnCat: https://github.com/Unkn0wnCat
.. _muued: https://github.com/muued
.. _philpagel: https://github.com/philpagel
.. _eingemaischt: https://github.com/eingemaischt
.. [1] Scanners with API Integration allow to push scanned documents directly to :ref:`Paperless API <api-file_uploads>`, sometimes referred to as Webhook or Document POST.
.. [2] Canon Multi Function Printers show strange behavior over SMB. They close and reopen the file after every page. It's recommended to tune the
:ref:`polling <configuration-polling>` and :ref:`inotify <configuration-inotify>` configuration values for your scanner. The scanner timeout is 3 minutes, so ``180`` is a good starting point.
Mobile phone software
=====================
You can use your phone to "scan" documents. The regular camera app will work, but may have too low contrast for OCR to work well. Apps specifically for scanning are recommended.
+-----------------------------+----------------+-----+-----+-----+-------+--------+------------------+
| Name | OS | Supports | Recommended By |
+-----------------------------+----------------+-----+-----+-----+-------+--------+------------------+
| | | FTP | NFS | SMB | Email | WebDAV | |
+=============================+================+=====+=====+=====+=======+========+==================+
| `Office Lens`_ | Android | ? | ? | ? | ? | ? | `jonaswinkler`_ |
+-----------------------------+----------------+-----+-----+-----+-------+--------+------------------+
| `Genius Scan`_ | Android | yes | no | yes | yes | yes | `hannahswain`_ |
+-----------------------------+----------------+-----+-----+-----+-------+--------+------------------+
| `OpenScan`_ | Android | no | no | no | no | no | `benjaminfrank`_ |
+-----------------------------+----------------+-----+-----+-----+-------+--------+------------------+
| `OCR Scanner - QuickScan`_ | iOS | no | no | no | no | yes | `holzhannes`_ |
+-----------------------------+----------------+-----+-----+-----+-------+--------+------------------+
On Android, you can use these applications in combination with one of the :ref:`Paperless-ngx compatible apps <usage-mobile_upload>` to "Share" the documents produced by these scanner apps with paperless. On iOS, you can share the scanned documents via iOS-Sharing to other mail, WebDav or FTP apps.
There is also an iOS Shortcut that allows you to directly upload text, PDF and image documents available here: https://www.icloud.com/shortcuts/d234abc0885040129d9d75fa45fe1154
Please note this only works for documents downloaded to iCloud / the device, in other words not directly from a URL.
.. _Office Lens: https://play.google.com/store/apps/details?id=com.microsoft.office.officelens
.. _Genius Scan: https://play.google.com/store/apps/details?id=com.thegrizzlylabs.geniusscan.free
.. _OCR Scanner - QuickScan: https://apps.apple.com/us/app/quickscan-scanner-text-ocr/id1513790291
.. _OpenScan: https://github.com/Ethereal-Developers-Inc/OpenScan
.. _hannahswain: https://github.com/hannahswain
.. _benjaminfrank: https://github.com/benjaminfrank
API Scanning Setup
==================
This sections contains information on how to set up scanners to post directly to :ref:`Paperless API <api-file_uploads>`.
Doxie Q2
--------
This part assumes your Doxie is connected to WiFi and you know its IP.
1. Open your Doxie web UI by navigating to its IP address
2. Navigate to Options -> Webhook
3. Set the *URL* to ``https://[your-paperless-ngx-instance]/api/documents/post_document/``
4. Set the *File Parameter Name* to ``document``
5. Add the username and password to the respective fields (Consider creating a user just for your Doxie)
6. Click *Submit* at the bottom of the page
Congrats, you can now scan directly from your Doxie to your Paperless-ngx instance!

View File

@ -224,6 +224,7 @@ Install Paperless from Docker Hub
You can utilize Docker secrets for some configuration settings by You can utilize Docker secrets for some configuration settings by
appending `_FILE` to some configuration values. This is supported currently appending `_FILE` to some configuration values. This is supported currently
only by: only by:
* PAPERLESS_DBUSER * PAPERLESS_DBUSER
* PAPERLESS_DBPASS * PAPERLESS_DBPASS
* PAPERLESS_SECRET_KEY * PAPERLESS_SECRET_KEY

View File

@ -816,7 +816,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">184</context> <context context-type="linenumber">185</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/save-view-config-dialog/save-view-config-dialog.component.html</context> <context context-type="sourcefile">src/app/components/document-list/save-view-config-dialog/save-view-config-dialog.component.html</context>
@ -1015,16 +1015,12 @@
<context context-type="linenumber">13</context> <context context-type="linenumber">13</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="2722549756198502062" datatype="html">
<source>Add item</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/input/select/select.component.html</context>
<context context-type="linenumber">11</context>
</context-group>
<note priority="1" from="description">Used for both types, correspondents, storage paths</note>
</trans-unit>
<trans-unit id="524422427194414813" datatype="html"> <trans-unit id="524422427194414813" datatype="html">
<source>Suggestions:</source> <source>Suggestions:</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/input/date/date.component.html</context>
<context context-type="linenumber">16</context>
</context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/input/select/select.component.html</context> <context context-type="sourcefile">src/app/components/common/input/select/select.component.html</context>
<context context-type="linenumber">30</context> <context context-type="linenumber">30</context>
@ -1034,6 +1030,14 @@
<context context-type="linenumber">42</context> <context context-type="linenumber">42</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="2722549756198502062" datatype="html">
<source>Add item</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/input/select/select.component.html</context>
<context context-type="linenumber">11</context>
</context-group>
<note priority="1" from="description">Used for both types, correspondents, storage paths</note>
</trans-unit>
<trans-unit id="6560126119609945418" datatype="html"> <trans-unit id="6560126119609945418" datatype="html">
<source>Add tag</source> <source>Add tag</source>
<context-group purpose="location"> <context-group purpose="location">
@ -1498,7 +1502,7 @@
<source>Correspondent</source> <source>Correspondent</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">78</context> <context context-type="linenumber">79</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
@ -1521,7 +1525,7 @@
<source>Document type</source> <source>Document type</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">80</context> <context context-type="linenumber">81</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
@ -1544,7 +1548,7 @@
<source>Storage path</source> <source>Storage path</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">82</context> <context context-type="linenumber">83</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
@ -1563,21 +1567,21 @@
<source>Default</source> <source>Default</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">83</context> <context context-type="linenumber">84</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="6205355627445317276" datatype="html"> <trans-unit id="6205355627445317276" datatype="html">
<source>Content</source> <source>Content</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">90</context> <context context-type="linenumber">91</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="218403386307979629" datatype="html"> <trans-unit id="218403386307979629" datatype="html">
<source>Metadata</source> <source>Metadata</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">99</context> <context context-type="linenumber">100</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/metadata-collapse/metadata-collapse.component.ts</context> <context context-type="sourcefile">src/app/components/document-detail/metadata-collapse/metadata-collapse.component.ts</context>
@ -1588,95 +1592,95 @@
<source>Date modified</source> <source>Date modified</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">105</context> <context context-type="linenumber">106</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="6392918669949841614" datatype="html"> <trans-unit id="6392918669949841614" datatype="html">
<source>Date added</source> <source>Date added</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">109</context> <context context-type="linenumber">110</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="146828917013192897" datatype="html"> <trans-unit id="146828917013192897" datatype="html">
<source>Media filename</source> <source>Media filename</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">113</context> <context context-type="linenumber">114</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="4500855521601039868" datatype="html"> <trans-unit id="4500855521601039868" datatype="html">
<source>Original filename</source> <source>Original filename</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">117</context> <context context-type="linenumber">118</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="7985558498848210210" datatype="html"> <trans-unit id="7985558498848210210" datatype="html">
<source>Original MD5 checksum</source> <source>Original MD5 checksum</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">121</context> <context context-type="linenumber">122</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="5888243105821763422" datatype="html"> <trans-unit id="5888243105821763422" datatype="html">
<source>Original file size</source> <source>Original file size</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">125</context> <context context-type="linenumber">126</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="2696647325713149563" datatype="html"> <trans-unit id="2696647325713149563" datatype="html">
<source>Original mime type</source> <source>Original mime type</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">129</context> <context context-type="linenumber">130</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="342875990758166588" datatype="html"> <trans-unit id="342875990758166588" datatype="html">
<source>Archive MD5 checksum</source> <source>Archive MD5 checksum</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">133</context> <context context-type="linenumber">134</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="6033581412811562084" datatype="html"> <trans-unit id="6033581412811562084" datatype="html">
<source>Archive file size</source> <source>Archive file size</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">137</context> <context context-type="linenumber">138</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="6992781481378431874" datatype="html"> <trans-unit id="6992781481378431874" datatype="html">
<source>Original document metadata</source> <source>Original document metadata</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">143</context> <context context-type="linenumber">144</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="2846565152091361585" datatype="html"> <trans-unit id="2846565152091361585" datatype="html">
<source>Archived document metadata</source> <source>Archived document metadata</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">144</context> <context context-type="linenumber">145</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="8191371354890763172" datatype="html"> <trans-unit id="8191371354890763172" datatype="html">
<source>Enter Password</source> <source>Enter Password</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">166</context> <context context-type="linenumber">167</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">202</context> <context context-type="linenumber">203</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="3807699453257291879" datatype="html"> <trans-unit id="3807699453257291879" datatype="html">
<source>Comments</source> <source>Comments</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">173</context> <context context-type="linenumber">174</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.html</context> <context context-type="sourcefile">src/app/components/manage/settings/settings.component.html</context>
@ -1687,14 +1691,14 @@
<source>Discard</source> <source>Discard</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">182</context> <context context-type="linenumber">183</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="5129524307369213584" datatype="html"> <trans-unit id="5129524307369213584" datatype="html">
<source>Save &amp; next</source> <source>Save &amp; next</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">183</context> <context context-type="linenumber">184</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="9021887951960049161" datatype="html"> <trans-unit id="9021887951960049161" datatype="html">

View File

@ -21,7 +21,7 @@
</div> </div>
<ul ngbNav class="order-sm-3"> <ul ngbNav class="order-sm-3">
<li ngbDropdown class="nav-item dropdown"> <li ngbDropdown class="nav-item dropdown">
<button class="btn" id="userDropdown" ngbDropdownToggle> <button class="btn border-0" id="userDropdown" ngbDropdownToggle>
<span class="small me-2 d-none d-sm-inline"> <span class="small me-2 d-none d-sm-inline">
{{this.settingsService.displayName}} {{this.settingsService.displayName}}
</span> </span>

View File

@ -6,7 +6,8 @@
Please enter a comment. Please enter a comment.
</div> </div>
</div> </div>
<div class="form-group mt-2 d-flex justify-content-end"> <div class="form-group mt-2 d-flex justify-content-end align-items-center">
<div *ngIf="networkActive" class="spinner-border spinner-border-sm fw-normal me-auto" role="status"></div>
<button type="button" class="btn btn-primary btn-sm" [disabled]="networkActive" (click)="addComment()" i18n>Add comment</button> <button type="button" class="btn btn-primary btn-sm" [disabled]="networkActive" (click)="addComment()" i18n>Add comment</button>
</div> </div>
</form> </form>

View File

@ -1,4 +1,4 @@
import { Component, Input, OnInit } from '@angular/core' import { Component, Input } from '@angular/core'
import { DocumentCommentsService } from 'src/app/services/rest/document-comments.service' import { DocumentCommentsService } from 'src/app/services/rest/document-comments.service'
import { PaperlessDocumentComment } from 'src/app/data/paperless-document-comment' import { PaperlessDocumentComment } from 'src/app/data/paperless-document-comment'
import { FormControl, FormGroup } from '@angular/forms' import { FormControl, FormGroup } from '@angular/forms'
@ -10,7 +10,7 @@ import { ToastService } from 'src/app/services/toast.service'
templateUrl: './document-comments.component.html', templateUrl: './document-comments.component.html',
styleUrls: ['./document-comments.component.scss'], styleUrls: ['./document-comments.component.scss'],
}) })
export class DocumentCommentsComponent implements OnInit { export class DocumentCommentsComponent {
commentForm: FormGroup = new FormGroup({ commentForm: FormGroup = new FormGroup({
newComment: new FormControl(''), newComment: new FormControl(''),
}) })
@ -19,19 +19,30 @@ export class DocumentCommentsComponent implements OnInit {
comments: PaperlessDocumentComment[] = [] comments: PaperlessDocumentComment[] = []
newCommentError: boolean = false newCommentError: boolean = false
private _documentId: number
@Input() @Input()
documentId: number set documentId(id: number) {
if (id != this._documentId) {
this._documentId = id
this.update()
}
}
constructor( constructor(
private commentsService: DocumentCommentsService, private commentsService: DocumentCommentsService,
private toastService: ToastService private toastService: ToastService
) {} ) {}
ngOnInit(): void { update(): void {
this.networkActive = true
this.commentsService this.commentsService
.getComments(this.documentId) .getComments(this._documentId)
.pipe(first()) .pipe(first())
.subscribe((comments) => (this.comments = comments)) .subscribe((comments) => {
this.comments = comments
this.networkActive = false
})
} }
addComment() { addComment() {
@ -45,7 +56,7 @@ export class DocumentCommentsComponent implements OnInit {
} }
this.newCommentError = false this.newCommentError = false
this.networkActive = true this.networkActive = true
this.commentsService.addComment(this.documentId, comment).subscribe({ this.commentsService.addComment(this._documentId, comment).subscribe({
next: (result) => { next: (result) => {
this.comments = result this.comments = result
this.commentForm.get('newComment').reset() this.commentForm.get('newComment').reset()
@ -61,7 +72,7 @@ export class DocumentCommentsComponent implements OnInit {
} }
deleteComment(commentId: number) { deleteComment(commentId: number) {
this.commentsService.deleteComment(this.documentId, commentId).subscribe({ this.commentsService.deleteComment(this._documentId, commentId).subscribe({
next: (result) => { next: (result) => {
this.comments = result this.comments = result
this.networkActive = false this.networkActive = false

View File

@ -80,7 +80,7 @@ a {
} }
.tags { .tags {
top: 0; top: .2rem;
right: 0; right: 0;
max-width: 80%; max-width: 80%;
row-gap: .2rem; row-gap: .2rem;

View File

@ -5,7 +5,7 @@ export const environment = {
apiBaseUrl: document.baseURI + 'api/', apiBaseUrl: document.baseURI + 'api/',
apiVersion: '2', apiVersion: '2',
appTitle: 'Paperless-ngx', appTitle: 'Paperless-ngx',
version: '1.8.1-dev', version: '1.9.0',
webSocketHost: window.location.host, webSocketHost: window.location.host,
webSocketProtocol: window.location.protocol == 'https:' ? 'wss:' : 'ws:', webSocketProtocol: window.location.protocol == 'https:' ? 'wss:' : 'ws:',
webSocketBaseUrl: base_url.pathname + 'ws/', webSocketBaseUrl: base_url.pathname + 'ws/',

File diff suppressed because it is too large Load Diff

View File

@ -345,7 +345,7 @@
<context context-type="sourcefile">src/app/app.component.ts</context> <context context-type="sourcefile">src/app/app.component.ts</context>
<context context-type="linenumber">146</context> <context context-type="linenumber">146</context>
</context-group> </context-group>
<target state="translated">Début de l'envoi …</target> <target state="translated">Début du téléversement...</target>
</trans-unit> </trans-unit>
<trans-unit id="2173456130768795374" datatype="html"> <trans-unit id="2173456130768795374" datatype="html">
<source>Paperless-ngx</source> <source>Paperless-ngx</source>

View File

@ -1451,7 +1451,7 @@
<context context-type="sourcefile">src/app/components/document-comments/document-comments.component.html</context> <context context-type="sourcefile">src/app/components/document-comments/document-comments.component.html</context>
<context context-type="linenumber">4</context> <context context-type="linenumber">4</context>
</context-group> </context-group>
<target state="needs-translation">Enter comment</target> <target state="translated">Vnesite komentar</target>
</trans-unit> </trans-unit>
<trans-unit id="4025397324401332794" datatype="html"> <trans-unit id="4025397324401332794" datatype="html">
<source> Please enter a comment. </source> <source> Please enter a comment. </source>
@ -1459,7 +1459,7 @@
<context context-type="sourcefile">src/app/components/document-comments/document-comments.component.html</context> <context context-type="sourcefile">src/app/components/document-comments/document-comments.component.html</context>
<context context-type="linenumber">5,7</context> <context context-type="linenumber">5,7</context>
</context-group> </context-group>
<target state="needs-translation"> Please enter a comment. </target> <target state="translated"> Prosimo, vpišite komentar. </target>
</trans-unit> </trans-unit>
<trans-unit id="2337485514607640701" datatype="html"> <trans-unit id="2337485514607640701" datatype="html">
<source>Add comment</source> <source>Add comment</source>
@ -1467,7 +1467,7 @@
<context context-type="sourcefile">src/app/components/document-comments/document-comments.component.html</context> <context context-type="sourcefile">src/app/components/document-comments/document-comments.component.html</context>
<context context-type="linenumber">10</context> <context context-type="linenumber">10</context>
</context-group> </context-group>
<target state="needs-translation">Add comment</target> <target state="translated">Dodaj komentar</target>
</trans-unit> </trans-unit>
<trans-unit id="5438997040668245251" datatype="html"> <trans-unit id="5438997040668245251" datatype="html">
<source>Error saving comment: <x id="PH" equiv-text="e.toString()"/></source> <source>Error saving comment: <x id="PH" equiv-text="e.toString()"/></source>
@ -1475,7 +1475,7 @@
<context context-type="sourcefile">src/app/components/document-comments/document-comments.component.ts</context> <context context-type="sourcefile">src/app/components/document-comments/document-comments.component.ts</context>
<context context-type="linenumber">57</context> <context context-type="linenumber">57</context>
</context-group> </context-group>
<target state="needs-translation">Error saving comment: <x id="PH" equiv-text="e.toString()"/></target> <target state="translated">Napaka pri shranjevanju komentarja: <x id="PH" equiv-text="e.toString()"/></target>
</trans-unit> </trans-unit>
<trans-unit id="7593210124183303626" datatype="html"> <trans-unit id="7593210124183303626" datatype="html">
<source>Error deleting comment: <x id="PH" equiv-text="e.toString()"/></source> <source>Error deleting comment: <x id="PH" equiv-text="e.toString()"/></source>
@ -1483,7 +1483,7 @@
<context context-type="sourcefile">src/app/components/document-comments/document-comments.component.ts</context> <context context-type="sourcefile">src/app/components/document-comments/document-comments.component.ts</context>
<context context-type="linenumber">72</context> <context context-type="linenumber">72</context>
</context-group> </context-group>
<target state="needs-translation">Error deleting comment: <x id="PH" equiv-text="e.toString()"/></target> <target state="translated">Napaka pri brisanju komentarja: <x id="PH" equiv-text="e.toString()"/></target>
</trans-unit> </trans-unit>
<trans-unit id="1407560924967345762" datatype="html"> <trans-unit id="1407560924967345762" datatype="html">
<source>Page</source> <source>Page</source>
@ -1859,7 +1859,7 @@
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.html</context> <context context-type="sourcefile">src/app/components/manage/settings/settings.component.html</context>
<context context-type="linenumber">128</context> <context context-type="linenumber">128</context>
</context-group> </context-group>
<target state="needs-translation">Comments</target> <target state="translated">Komentarji</target>
</trans-unit> </trans-unit>
<trans-unit id="3823219296477075982" datatype="html"> <trans-unit id="3823219296477075982" datatype="html">
<source>Discard</source> <source>Discard</source>
@ -2245,7 +2245,7 @@
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">339</context> <context context-type="linenumber">339</context>
</context-group> </context-group>
<target state="needs-translation">This operation will assign the storage path "<x id="PH" equiv-text="storagePath.name"/>" to <x id="PH_1" equiv-text="this.list.selected.size"/> selected document(s).</target> <target state="translated">Ta operacija bo dodelila pot shranjevanja "<x id="PH" equiv-text="storagePath.name"/>" v <x id="PH_1" equiv-text="this.list.selected.size"/> izbran dokument/e.</target>
</trans-unit> </trans-unit>
<trans-unit id="60728365335056946" datatype="html"> <trans-unit id="60728365335056946" datatype="html">
<source>This operation will remove the storage path from <x id="PH" equiv-text="this.list.selected.size"/> selected document(s).</source> <source>This operation will remove the storage path from <x id="PH" equiv-text="this.list.selected.size"/> selected document(s).</source>
@ -2253,7 +2253,7 @@
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">341</context> <context context-type="linenumber">341</context>
</context-group> </context-group>
<target state="needs-translation">This operation will remove the storage path from <x id="PH" equiv-text="this.list.selected.size"/> selected document(s).</target> <target state="translated">Ta operacija bo odstranila pot shranjevanja iz <x id="PH" equiv-text="this.list.selected.size"/> izbranih dokumentov.</target>
</trans-unit> </trans-unit>
<trans-unit id="749430623564850405" datatype="html"> <trans-unit id="749430623564850405" datatype="html">
<source>Delete confirm</source> <source>Delete confirm</source>
@ -2285,7 +2285,7 @@
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">388</context> <context context-type="linenumber">388</context>
</context-group> </context-group>
<target state="needs-translation">This operation will permanently redo OCR for <x id="PH" equiv-text="this.list.selected.size"/> selected document(s).</target> <target state="translated">Ta operacija bo trajno obnovila OCR za <x id="PH" equiv-text="this.list.selected.size"/> izbrane dokumente.</target>
</trans-unit> </trans-unit>
<trans-unit id="8076495233090006322" datatype="html"> <trans-unit id="8076495233090006322" datatype="html">
<source>Filter by correspondent</source> <source>Filter by correspondent</source>
@ -2373,7 +2373,7 @@
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">180</context> <context context-type="linenumber">180</context>
</context-group> </context-group>
<target state="needs-translation">Filter by document type</target> <target state="translated">Filtriraj po vrsti dokumenta</target>
</trans-unit> </trans-unit>
<trans-unit id="157572966557284263" datatype="html"> <trans-unit id="157572966557284263" datatype="html">
<source>Filter by storage path</source> <source>Filter by storage path</source>
@ -2385,7 +2385,7 @@
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">185</context> <context context-type="linenumber">185</context>
</context-group> </context-group>
<target state="needs-translation">Filter by storage path</target> <target state="translated">Filtriraj po poti shrambe</target>
</trans-unit> </trans-unit>
<trans-unit id="3727324658595204357" datatype="html"> <trans-unit id="3727324658595204357" datatype="html">
<source>Created: <x id="INTERPOLATION" equiv-text="{{ document.created | customDate }}"/></source> <source>Created: <x id="INTERPOLATION" equiv-text="{{ document.created | customDate }}"/></source>
@ -2397,7 +2397,7 @@
<context context-type="sourcefile">src/app/components/document-list/document-card-small/document-card-small.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-card-small/document-card-small.component.html</context>
<context context-type="linenumber">48</context> <context context-type="linenumber">48</context>
</context-group> </context-group>
<target state="needs-translation">Created: <x id="INTERPOLATION" equiv-text="{{ document.created | customDate }}"/></target> <target state="translated">Ustvarjeno: <x id="INTERPOLATION" equiv-text="{{ document.created | customDate }}"/></target>
</trans-unit> </trans-unit>
<trans-unit id="2030261243264601523" datatype="html"> <trans-unit id="2030261243264601523" datatype="html">
<source>Added: <x id="INTERPOLATION" equiv-text="{{ document.added | customDate }}"/></source> <source>Added: <x id="INTERPOLATION" equiv-text="{{ document.added | customDate }}"/></source>
@ -2409,7 +2409,7 @@
<context context-type="sourcefile">src/app/components/document-list/document-card-small/document-card-small.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-card-small/document-card-small.component.html</context>
<context context-type="linenumber">49</context> <context context-type="linenumber">49</context>
</context-group> </context-group>
<target state="needs-translation">Added: <x id="INTERPOLATION" equiv-text="{{ document.added | customDate }}"/></target> <target state="translated">Dodano: <x id="INTERPOLATION" equiv-text="{{ document.added | customDate }}"/></target>
</trans-unit> </trans-unit>
<trans-unit id="4235671847487610290" datatype="html"> <trans-unit id="4235671847487610290" datatype="html">
<source>Modified: <x id="INTERPOLATION" equiv-text="{{ document.modified | customDate }}"/></source> <source>Modified: <x id="INTERPOLATION" equiv-text="{{ document.modified | customDate }}"/></source>
@ -2421,7 +2421,7 @@
<context context-type="sourcefile">src/app/components/document-list/document-card-small/document-card-small.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-card-small/document-card-small.component.html</context>
<context context-type="linenumber">50</context> <context context-type="linenumber">50</context>
</context-group> </context-group>
<target state="needs-translation">Modified: <x id="INTERPOLATION" equiv-text="{{ document.modified | customDate }}"/></target> <target state="translated">Spremenjeno: <x id="INTERPOLATION" equiv-text="{{ document.modified | customDate }}"/></target>
</trans-unit> </trans-unit>
<trans-unit id="2332107018974972998" datatype="html"> <trans-unit id="2332107018974972998" datatype="html">
<source>Score:</source> <source>Score:</source>
@ -2437,7 +2437,7 @@
<context context-type="sourcefile">src/app/components/document-list/document-card-small/document-card-small.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-card-small/document-card-small.component.html</context>
<context context-type="linenumber">14</context> <context context-type="linenumber">14</context>
</context-group> </context-group>
<target state="needs-translation">Toggle tag filter</target> <target state="translated">Preklopi filter oznak</target>
</trans-unit> </trans-unit>
<trans-unit id="4648526799630820486" datatype="html"> <trans-unit id="4648526799630820486" datatype="html">
<source>Toggle correspondent filter</source> <source>Toggle correspondent filter</source>
@ -2445,7 +2445,7 @@
<context context-type="sourcefile">src/app/components/document-list/document-card-small/document-card-small.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-card-small/document-card-small.component.html</context>
<context context-type="linenumber">24</context> <context context-type="linenumber">24</context>
</context-group> </context-group>
<target state="needs-translation">Toggle correspondent filter</target> <target state="translated">Preklop korespondenčnega filtra</target>
</trans-unit> </trans-unit>
<trans-unit id="5319701482646590642" datatype="html"> <trans-unit id="5319701482646590642" datatype="html">
<source>Toggle document type filter</source> <source>Toggle document type filter</source>
@ -2453,7 +2453,7 @@
<context context-type="sourcefile">src/app/components/document-list/document-card-small/document-card-small.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-card-small/document-card-small.component.html</context>
<context context-type="linenumber">31</context> <context context-type="linenumber">31</context>
</context-group> </context-group>
<target state="needs-translation">Toggle document type filter</target> <target state="translated">Preklop filtra vrste dokumenta</target>
</trans-unit> </trans-unit>
<trans-unit id="8950368321707344185" datatype="html"> <trans-unit id="8950368321707344185" datatype="html">
<source>Toggle storage path filter</source> <source>Toggle storage path filter</source>
@ -2461,7 +2461,7 @@
<context context-type="sourcefile">src/app/components/document-list/document-card-small/document-card-small.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-card-small/document-card-small.component.html</context>
<context context-type="linenumber">38</context> <context context-type="linenumber">38</context>
</context-group> </context-group>
<target state="needs-translation">Toggle storage path filter</target> <target state="translated">Preklop filtra poti za shranjevanje</target>
</trans-unit> </trans-unit>
<trans-unit id="5145213156408463657" datatype="html"> <trans-unit id="5145213156408463657" datatype="html">
<source>Select none</source> <source>Select none</source>
@ -2589,7 +2589,7 @@
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">175</context> <context context-type="linenumber">175</context>
</context-group> </context-group>
<target state="needs-translation">Edit document</target> <target state="translated">Uredi dokument</target>
</trans-unit> </trans-unit>
<trans-unit id="2155249406916744630" datatype="html"> <trans-unit id="2155249406916744630" datatype="html">
<source>View &quot;<x id="PH" equiv-text="this.list.activeSavedViewTitle"/>&quot; saved successfully.</source> <source>View &quot;<x id="PH" equiv-text="this.list.activeSavedViewTitle"/>&quot; saved successfully.</source>
@ -2709,7 +2709,7 @@
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context> <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context>
<context context-type="linenumber">164</context> <context context-type="linenumber">164</context>
</context-group> </context-group>
<target state="needs-translation">equals</target> <target state="translated">je enako</target>
</trans-unit> </trans-unit>
<trans-unit id="5325481293405718739" datatype="html"> <trans-unit id="5325481293405718739" datatype="html">
<source>is empty</source> <source>is empty</source>
@ -2717,7 +2717,7 @@
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context> <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context>
<context context-type="linenumber">168</context> <context context-type="linenumber">168</context>
</context-group> </context-group>
<target state="needs-translation">is empty</target> <target state="translated">je prazno</target>
</trans-unit> </trans-unit>
<trans-unit id="6166785695326182482" datatype="html"> <trans-unit id="6166785695326182482" datatype="html">
<source>is not empty</source> <source>is not empty</source>
@ -2725,7 +2725,7 @@
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context> <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context>
<context context-type="linenumber">172</context> <context context-type="linenumber">172</context>
</context-group> </context-group>
<target state="needs-translation">is not empty</target> <target state="translated">ni prazno</target>
</trans-unit> </trans-unit>
<trans-unit id="4686622206659266699" datatype="html"> <trans-unit id="4686622206659266699" datatype="html">
<source>greater than</source> <source>greater than</source>
@ -2733,7 +2733,7 @@
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context> <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context>
<context context-type="linenumber">176</context> <context context-type="linenumber">176</context>
</context-group> </context-group>
<target state="needs-translation">greater than</target> <target state="translated">večje kot</target>
</trans-unit> </trans-unit>
<trans-unit id="8014012170270529279" datatype="html"> <trans-unit id="8014012170270529279" datatype="html">
<source>less than</source> <source>less than</source>
@ -2741,7 +2741,7 @@
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context> <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context>
<context context-type="linenumber">180</context> <context context-type="linenumber">180</context>
</context-group> </context-group>
<target state="needs-translation">less than</target> <target state="translated">manj kot</target>
</trans-unit> </trans-unit>
<trans-unit id="7210076240260527720" datatype="html"> <trans-unit id="7210076240260527720" datatype="html">
<source>Save current view</source> <source>Save current view</source>
@ -2805,7 +2805,7 @@
<context context-type="sourcefile">src/app/components/manage/correspondent-list/correspondent-list.component.ts</context> <context context-type="sourcefile">src/app/components/manage/correspondent-list/correspondent-list.component.ts</context>
<context context-type="linenumber">34</context> <context context-type="linenumber">34</context>
</context-group> </context-group>
<target state="needs-translation">correspondents</target> <target state="translated">dopisniki</target>
</trans-unit> </trans-unit>
<trans-unit id="6360600151505327572" datatype="html"> <trans-unit id="6360600151505327572" datatype="html">
<source>Last used</source> <source>Last used</source>
@ -2837,7 +2837,7 @@
<context context-type="sourcefile">src/app/components/manage/document-type-list/document-type-list.component.ts</context> <context context-type="sourcefile">src/app/components/manage/document-type-list/document-type-list.component.ts</context>
<context context-type="linenumber">31</context> <context context-type="linenumber">31</context>
</context-group> </context-group>
<target state="needs-translation">document types</target> <target state="translated">vrste dokumentov</target>
</trans-unit> </trans-unit>
<trans-unit id="4990731724078522539" datatype="html"> <trans-unit id="4990731724078522539" datatype="html">
<source>Do you really want to delete the document type &quot;<x id="PH" equiv-text="object.name"/>&quot;?</source> <source>Do you really want to delete the document type &quot;<x id="PH" equiv-text="object.name"/>&quot;?</source>
@ -2965,7 +2965,7 @@
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context> <context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
<context context-type="linenumber">74</context> <context context-type="linenumber">74</context>
</context-group> </context-group>
<target state="needs-translation">{VAR_PLURAL, plural, =1 {One <x id="INTERPOLATION"/>} other {<x id="INTERPOLATION_1"/> total <x id="INTERPOLATION_2"/>}}</target> <target state="translated">{VAR_PLURAL, plural, =1 {Eden <x id="INTERPOLATION"/>} other {<x id="INTERPOLATION_1"/> skupno <x id="INTERPOLATION_2"/>}}</target>
</trans-unit> </trans-unit>
<trans-unit id="810888510148304696" datatype="html"> <trans-unit id="810888510148304696" datatype="html">
<source>Automatic</source> <source>Automatic</source>
@ -2985,7 +2985,7 @@
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.ts</context> <context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.ts</context>
<context context-type="linenumber">140</context> <context context-type="linenumber">140</context>
</context-group> </context-group>
<target state="needs-translation">Do you really want to delete the <x id="PH" equiv-text="this.typeName"/>?</target> <target state="translated">Ali res želite izbrisati <x id="PH" equiv-text="this.typeName"/>?</target>
</trans-unit> </trans-unit>
<trans-unit id="8371896857609524947" datatype="html"> <trans-unit id="8371896857609524947" datatype="html">
<source>Associated documents will not be deleted.</source> <source>Associated documents will not be deleted.</source>
@ -3001,7 +3001,7 @@
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.ts</context> <context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.ts</context>
<context context-type="linenumber">168,170</context> <context context-type="linenumber">168,170</context>
</context-group> </context-group>
<target state="needs-translation">Error while deleting element: <x id="PH" equiv-text="JSON.stringify( error.error )"/></target> <target state="translated">Napaka pri brisanju elementa: <x id="PH" equiv-text="JSON.stringify( error.error )"/></target>
</trans-unit> </trans-unit>
<trans-unit id="6439365426343089851" datatype="html"> <trans-unit id="6439365426343089851" datatype="html">
<source>General</source> <source>General</source>
@ -3009,7 +3009,7 @@
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.html</context> <context context-type="sourcefile">src/app/components/manage/settings/settings.component.html</context>
<context context-type="linenumber">10</context> <context context-type="linenumber">10</context>
</context-group> </context-group>
<target state="needs-translation">General</target> <target state="translated">Splošno</target>
</trans-unit> </trans-unit>
<trans-unit id="8671234314555525900" datatype="html"> <trans-unit id="8671234314555525900" datatype="html">
<source>Appearance</source> <source>Appearance</source>
@ -3145,7 +3145,7 @@
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.html</context> <context context-type="sourcefile">src/app/components/manage/settings/settings.component.html</context>
<context context-type="linenumber">105</context> <context context-type="linenumber">105</context>
</context-group> </context-group>
<target state="needs-translation">Theme Color</target> <target state="translated">Barve teme</target>
</trans-unit> </trans-unit>
<trans-unit id="7808756054397155068" datatype="html"> <trans-unit id="7808756054397155068" datatype="html">
<source>Reset</source> <source>Reset</source>
@ -3153,7 +3153,7 @@
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.html</context> <context context-type="sourcefile">src/app/components/manage/settings/settings.component.html</context>
<context context-type="linenumber">114</context> <context context-type="linenumber">114</context>
</context-group> </context-group>
<target state="needs-translation">Reset</target> <target state="translated">Ponastavi</target>
</trans-unit> </trans-unit>
<trans-unit id="8508424367627989968" datatype="html"> <trans-unit id="8508424367627989968" datatype="html">
<source>Bulk editing</source> <source>Bulk editing</source>
@ -3193,7 +3193,7 @@
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.html</context> <context context-type="sourcefile">src/app/components/manage/settings/settings.component.html</context>
<context context-type="linenumber">132</context> <context context-type="linenumber">132</context>
</context-group> </context-group>
<target state="needs-translation">Enable comments</target> <target state="translated">Omogoči komentarje</target>
</trans-unit> </trans-unit>
<trans-unit id="5851560788527570644" datatype="html"> <trans-unit id="5851560788527570644" datatype="html">
<source>Notifications</source> <source>Notifications</source>
@ -3281,7 +3281,7 @@
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context> <context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
<context context-type="linenumber">253</context> <context context-type="linenumber">253</context>
</context-group> </context-group>
<target state="needs-translation">Settings saved</target> <target state="translated">Nastavitve shranjene</target>
</trans-unit> </trans-unit>
<trans-unit id="7217000812750597833" datatype="html"> <trans-unit id="7217000812750597833" datatype="html">
<source>Settings were saved successfully.</source> <source>Settings were saved successfully.</source>
@ -3289,7 +3289,7 @@
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context> <context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
<context context-type="linenumber">254</context> <context context-type="linenumber">254</context>
</context-group> </context-group>
<target state="needs-translation">Settings were saved successfully.</target> <target state="translated">Nastavitve uspešno shranjene.</target>
</trans-unit> </trans-unit>
<trans-unit id="525012668859298131" datatype="html"> <trans-unit id="525012668859298131" datatype="html">
<source>Settings were saved successfully. Reload is required to apply some changes.</source> <source>Settings were saved successfully. Reload is required to apply some changes.</source>
@ -3297,7 +3297,7 @@
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context> <context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
<context context-type="linenumber">258</context> <context context-type="linenumber">258</context>
</context-group> </context-group>
<target state="needs-translation">Settings were saved successfully. Reload is required to apply some changes.</target> <target state="translated">Nastavitve so bile uspešno shranjene. Za uveljavitev nekaterih sprememb je potreben ponovni zagon.</target>
</trans-unit> </trans-unit>
<trans-unit id="8491974984518503778" datatype="html"> <trans-unit id="8491974984518503778" datatype="html">
<source>Reload now</source> <source>Reload now</source>
@ -3305,7 +3305,7 @@
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context> <context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
<context context-type="linenumber">259</context> <context context-type="linenumber">259</context>
</context-group> </context-group>
<target state="needs-translation">Reload now</target> <target state="translated">Ponovno zaženi</target>
</trans-unit> </trans-unit>
<trans-unit id="3011185103048412841" datatype="html"> <trans-unit id="3011185103048412841" datatype="html">
<source>An error occurred while saving settings.</source> <source>An error occurred while saving settings.</source>
@ -3313,7 +3313,7 @@
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context> <context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
<context context-type="linenumber">269</context> <context context-type="linenumber">269</context>
</context-group> </context-group>
<target state="needs-translation">An error occurred while saving settings.</target> <target state="translated">Prišlo je do napake ob shranjevanju nastavitev.</target>
</trans-unit> </trans-unit>
<trans-unit id="6839066544204061364" datatype="html"> <trans-unit id="6839066544204061364" datatype="html">
<source>Use system language</source> <source>Use system language</source>
@ -3337,7 +3337,7 @@
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context> <context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
<context context-type="linenumber">304,306</context> <context context-type="linenumber">304,306</context>
</context-group> </context-group>
<target state="needs-translation">Error while storing settings on server: <x id="PH" equiv-text="JSON.stringify( error.error )"/></target> <target state="translated">Napaka pri shranjevanju nastavitev na strežnik: <x id="PH" equiv-text="JSON.stringify( error.error )"/></target>
</trans-unit> </trans-unit>
<trans-unit id="5101757640976222639" datatype="html"> <trans-unit id="5101757640976222639" datatype="html">
<source>storage path</source> <source>storage path</source>
@ -3345,7 +3345,7 @@
<context context-type="sourcefile">src/app/components/manage/storage-path-list/storage-path-list.component.ts</context> <context context-type="sourcefile">src/app/components/manage/storage-path-list/storage-path-list.component.ts</context>
<context context-type="linenumber">30</context> <context context-type="linenumber">30</context>
</context-group> </context-group>
<target state="needs-translation">storage path</target> <target state="translated">pot do shrambe</target>
</trans-unit> </trans-unit>
<trans-unit id="22235115124223314" datatype="html"> <trans-unit id="22235115124223314" datatype="html">
<source>storage paths</source> <source>storage paths</source>
@ -3353,7 +3353,7 @@
<context context-type="sourcefile">src/app/components/manage/storage-path-list/storage-path-list.component.ts</context> <context context-type="sourcefile">src/app/components/manage/storage-path-list/storage-path-list.component.ts</context>
<context context-type="linenumber">31</context> <context context-type="linenumber">31</context>
</context-group> </context-group>
<target state="needs-translation">storage paths</target> <target state="translated">poti do shrambe</target>
</trans-unit> </trans-unit>
<trans-unit id="1569070683025071137" datatype="html"> <trans-unit id="1569070683025071137" datatype="html">
<source>Do you really want to delete the storage path &quot;<x id="PH" equiv-text="object.name"/>&quot;?</source> <source>Do you really want to delete the storage path &quot;<x id="PH" equiv-text="object.name"/>&quot;?</source>
@ -3361,7 +3361,7 @@
<context context-type="sourcefile">src/app/components/manage/storage-path-list/storage-path-list.component.ts</context> <context context-type="sourcefile">src/app/components/manage/storage-path-list/storage-path-list.component.ts</context>
<context context-type="linenumber">45</context> <context context-type="linenumber">45</context>
</context-group> </context-group>
<target state="needs-translation">Do you really want to delete the storage path "<x id="PH" equiv-text="object.name"/>"?</target> <target state="translated">Ali res želite izbrisati pot shranjevanja "<x id="PH" equiv-text="object.name"/>"?</target>
</trans-unit> </trans-unit>
<trans-unit id="6402703264596649214" datatype="html"> <trans-unit id="6402703264596649214" datatype="html">
<source>tag</source> <source>tag</source>
@ -3369,7 +3369,7 @@
<context context-type="sourcefile">src/app/components/manage/tag-list/tag-list.component.ts</context> <context context-type="sourcefile">src/app/components/manage/tag-list/tag-list.component.ts</context>
<context context-type="linenumber">30</context> <context context-type="linenumber">30</context>
</context-group> </context-group>
<target state="needs-translation">tag</target> <target state="translated">oznaka</target>
</trans-unit> </trans-unit>
<trans-unit id="4975748273657042999" datatype="html"> <trans-unit id="4975748273657042999" datatype="html">
<source>tags</source> <source>tags</source>
@ -3377,7 +3377,7 @@
<context context-type="sourcefile">src/app/components/manage/tag-list/tag-list.component.ts</context> <context context-type="sourcefile">src/app/components/manage/tag-list/tag-list.component.ts</context>
<context context-type="linenumber">31</context> <context context-type="linenumber">31</context>
</context-group> </context-group>
<target state="needs-translation">tags</target> <target state="translated">oznake</target>
</trans-unit> </trans-unit>
<trans-unit id="93754014749412887" datatype="html"> <trans-unit id="93754014749412887" datatype="html">
<source>Do you really want to delete the tag &quot;<x id="PH" equiv-text="object.name"/>&quot;?</source> <source>Do you really want to delete the tag &quot;<x id="PH" equiv-text="object.name"/>&quot;?</source>
@ -3393,7 +3393,7 @@
<context context-type="sourcefile">src/app/components/manage/tasks/tasks.component.html</context> <context context-type="sourcefile">src/app/components/manage/tasks/tasks.component.html</context>
<context context-type="linenumber">1</context> <context context-type="linenumber">1</context>
</context-group> </context-group>
<target state="needs-translation">File Tasks</target> <target state="translated">Datotečne naloge</target>
</trans-unit> </trans-unit>
<trans-unit id="103921551219467537" datatype="html"> <trans-unit id="103921551219467537" datatype="html">
<source>Clear selection</source> <source>Clear selection</source>
@ -3401,7 +3401,7 @@
<context context-type="sourcefile">src/app/components/manage/tasks/tasks.component.html</context> <context context-type="sourcefile">src/app/components/manage/tasks/tasks.component.html</context>
<context context-type="linenumber">6</context> <context context-type="linenumber">6</context>
</context-group> </context-group>
<target state="needs-translation">Clear selection</target> <target state="translated">Počisti izbiro</target>
</trans-unit> </trans-unit>
<trans-unit id="187187500641108332" datatype="html"> <trans-unit id="187187500641108332" datatype="html">
<source> <source>
@ -3411,7 +3411,7 @@
<context context-type="sourcefile">src/app/components/manage/tasks/tasks.component.html</context> <context context-type="sourcefile">src/app/components/manage/tasks/tasks.component.html</context>
<context context-type="linenumber">11</context> <context context-type="linenumber">11</context>
</context-group> </context-group>
<target state="needs-translation"><x id="INTERPOLATION" equiv-text="{{dismissButtonText}}"/></target> <target state="translated"><x id="INTERPOLATION" equiv-text="{{dismissButtonText}}"/></target>
</trans-unit> </trans-unit>
<trans-unit id="1102717806459547726" datatype="html"> <trans-unit id="1102717806459547726" datatype="html">
<source>Refresh</source> <source>Refresh</source>
@ -3419,7 +3419,7 @@
<context context-type="sourcefile">src/app/components/manage/tasks/tasks.component.html</context> <context context-type="sourcefile">src/app/components/manage/tasks/tasks.component.html</context>
<context context-type="linenumber">20</context> <context context-type="linenumber">20</context>
</context-group> </context-group>
<target state="needs-translation">Refresh</target> <target state="translated">Osveži</target>
</trans-unit> </trans-unit>
<trans-unit id="5968132631442328843" datatype="html"> <trans-unit id="5968132631442328843" datatype="html">
<source>Results</source> <source>Results</source>
@ -3427,7 +3427,7 @@
<context context-type="sourcefile">src/app/components/manage/tasks/tasks.component.html</context> <context context-type="sourcefile">src/app/components/manage/tasks/tasks.component.html</context>
<context context-type="linenumber">42</context> <context context-type="linenumber">42</context>
</context-group> </context-group>
<target state="needs-translation">Results</target> <target state="translated">Rezultat</target>
</trans-unit> </trans-unit>
<trans-unit id="8958063833276423847" datatype="html"> <trans-unit id="8958063833276423847" datatype="html">
<source>click for full output</source> <source>click for full output</source>
@ -3435,7 +3435,7 @@
<context context-type="sourcefile">src/app/components/manage/tasks/tasks.component.html</context> <context context-type="sourcefile">src/app/components/manage/tasks/tasks.component.html</context>
<context context-type="linenumber">66</context> <context context-type="linenumber">66</context>
</context-group> </context-group>
<target state="needs-translation">click for full output</target> <target state="translated">kliknite za celoten izpis</target>
</trans-unit> </trans-unit>
<trans-unit id="1536087519743707362" datatype="html"> <trans-unit id="1536087519743707362" datatype="html">
<source>Dismiss</source> <source>Dismiss</source>
@ -3447,7 +3447,7 @@
<context context-type="sourcefile">src/app/components/manage/tasks/tasks.component.ts</context> <context context-type="sourcefile">src/app/components/manage/tasks/tasks.component.ts</context>
<context context-type="linenumber">54</context> <context context-type="linenumber">54</context>
</context-group> </context-group>
<target state="needs-translation">Dismiss</target> <target state="translated">Opusti</target>
</trans-unit> </trans-unit>
<trans-unit id="6798650225457993016" datatype="html"> <trans-unit id="6798650225457993016" datatype="html">
<source>Failed <x id="START_TAG_SPAN" ctype="x-span" equiv-text="&lt;span *ngIf=&quot;tasksService.failedFileTasks.length &gt; 0&quot; class=&quot;badge bg-danger ms-1&quot;&gt;"/><x id="INTERPOLATION" equiv-text="{{tasksService.failedFileTasks.length}}"/><x id="CLOSE_TAG_SPAN" ctype="x-span" equiv-text="&lt;/span&gt;"/></source> <source>Failed <x id="START_TAG_SPAN" ctype="x-span" equiv-text="&lt;span *ngIf=&quot;tasksService.failedFileTasks.length &gt; 0&quot; class=&quot;badge bg-danger ms-1&quot;&gt;"/><x id="INTERPOLATION" equiv-text="{{tasksService.failedFileTasks.length}}"/><x id="CLOSE_TAG_SPAN" ctype="x-span" equiv-text="&lt;/span&gt;"/></source>
@ -3455,7 +3455,7 @@
<context context-type="sourcefile">src/app/components/manage/tasks/tasks.component.html</context> <context context-type="sourcefile">src/app/components/manage/tasks/tasks.component.html</context>
<context context-type="linenumber">96</context> <context context-type="linenumber">96</context>
</context-group> </context-group>
<target state="needs-translation">Failed <x id="START_TAG_SPAN" ctype="x-span" equiv-text="&lt;span *ngIf=&quot;tasksService.failedFileTasks.length &gt; 0&quot; class=&quot;badge bg-danger ms-1&quot;&gt;"/><x id="INTERPOLATION" equiv-text="{{tasksService.failedFileTasks.length}}"/><x id="CLOSE_TAG_SPAN" ctype="x-span" equiv-text="&lt;/span&gt;"/></target> <target state="translated">Neuspešno <x id="START_TAG_SPAN" ctype="x-span" equiv-text="&lt;span *ngIf=&quot;tasksService.failedFileTasks.length &gt; 0&quot; class=&quot;badge bg-danger ms-1&quot;&gt;"/><x id="INTERPOLATION" equiv-text="{{tasksService.failedFileTasks.length}}"/><x id="CLOSE_TAG_SPAN" ctype="x-span" equiv-text="&lt;/span&gt;"/></target>
</trans-unit> </trans-unit>
<trans-unit id="2352193508676933865" datatype="html"> <trans-unit id="2352193508676933865" datatype="html">
<source>Complete <x id="START_TAG_SPAN" ctype="x-span" equiv-text="&lt;span *ngIf=&quot;tasksService.completedFileTasks.length &gt; 0&quot; class=&quot;badge bg-secondary ms-1&quot;&gt;"/><x id="INTERPOLATION" equiv-text="{{tasksService.completedFileTasks.length}}"/><x id="CLOSE_TAG_SPAN" ctype="x-span" equiv-text="&lt;/span&gt;"/></source> <source>Complete <x id="START_TAG_SPAN" ctype="x-span" equiv-text="&lt;span *ngIf=&quot;tasksService.completedFileTasks.length &gt; 0&quot; class=&quot;badge bg-secondary ms-1&quot;&gt;"/><x id="INTERPOLATION" equiv-text="{{tasksService.completedFileTasks.length}}"/><x id="CLOSE_TAG_SPAN" ctype="x-span" equiv-text="&lt;/span&gt;"/></source>
@ -3463,7 +3463,7 @@
<context context-type="sourcefile">src/app/components/manage/tasks/tasks.component.html</context> <context context-type="sourcefile">src/app/components/manage/tasks/tasks.component.html</context>
<context context-type="linenumber">102</context> <context context-type="linenumber">102</context>
</context-group> </context-group>
<target state="needs-translation">Complete <x id="START_TAG_SPAN" ctype="x-span" equiv-text="&lt;span *ngIf=&quot;tasksService.completedFileTasks.length &gt; 0&quot; class=&quot;badge bg-secondary ms-1&quot;&gt;"/><x id="INTERPOLATION" equiv-text="{{tasksService.completedFileTasks.length}}"/><x id="CLOSE_TAG_SPAN" ctype="x-span" equiv-text="&lt;/span&gt;"/></target> <target state="translated">Dokončano <x id="START_TAG_SPAN" ctype="x-span" equiv-text="&lt;span *ngIf=&quot;tasksService.completedFileTasks.length &gt; 0&quot; class=&quot;badge bg-secondary ms-1&quot;&gt;"/><x id="INTERPOLATION" equiv-text="{{tasksService.completedFileTasks.length}}"/><x id="CLOSE_TAG_SPAN" ctype="x-span" equiv-text="&lt;/span&gt;"/></target>
</trans-unit> </trans-unit>
<trans-unit id="1697296301417588213" datatype="html"> <trans-unit id="1697296301417588213" datatype="html">
<source>Started <x id="START_TAG_SPAN" ctype="x-span" equiv-text="&lt;span *ngIf=&quot;tasksService.startedFileTasks.length &gt; 0&quot; class=&quot;badge bg-secondary ms-1&quot;&gt;"/><x id="INTERPOLATION" equiv-text="{{tasksService.startedFileTasks.length}}"/><x id="CLOSE_TAG_SPAN" ctype="x-span" equiv-text="&lt;/span&gt;"/></source> <source>Started <x id="START_TAG_SPAN" ctype="x-span" equiv-text="&lt;span *ngIf=&quot;tasksService.startedFileTasks.length &gt; 0&quot; class=&quot;badge bg-secondary ms-1&quot;&gt;"/><x id="INTERPOLATION" equiv-text="{{tasksService.startedFileTasks.length}}"/><x id="CLOSE_TAG_SPAN" ctype="x-span" equiv-text="&lt;/span&gt;"/></source>
@ -3471,7 +3471,7 @@
<context context-type="sourcefile">src/app/components/manage/tasks/tasks.component.html</context> <context context-type="sourcefile">src/app/components/manage/tasks/tasks.component.html</context>
<context context-type="linenumber">108</context> <context context-type="linenumber">108</context>
</context-group> </context-group>
<target state="needs-translation">Started <x id="START_TAG_SPAN" ctype="x-span" equiv-text="&lt;span *ngIf=&quot;tasksService.startedFileTasks.length &gt; 0&quot; class=&quot;badge bg-secondary ms-1&quot;&gt;"/><x id="INTERPOLATION" equiv-text="{{tasksService.startedFileTasks.length}}"/><x id="CLOSE_TAG_SPAN" ctype="x-span" equiv-text="&lt;/span&gt;"/></target> <target state="translated">Začetek <x id="START_TAG_SPAN" ctype="x-span" equiv-text="&lt;span *ngIf=&quot;tasksService.startedFileTasks.length &gt; 0&quot; class=&quot;badge bg-secondary ms-1&quot;&gt;"/><x id="INTERPOLATION" equiv-text="{{tasksService.startedFileTasks.length}}"/><x id="CLOSE_TAG_SPAN" ctype="x-span" equiv-text="&lt;/span&gt;"/></target>
</trans-unit> </trans-unit>
<trans-unit id="6517676116023827583" datatype="html"> <trans-unit id="6517676116023827583" datatype="html">
<source>Queued <x id="START_TAG_SPAN" ctype="x-span" equiv-text="&lt;span *ngIf=&quot;tasksService.queuedFileTasks.length &gt; 0&quot; class=&quot;badge bg-secondary ms-1&quot;&gt;"/><x id="INTERPOLATION" equiv-text="{{tasksService.queuedFileTasks.length}}"/><x id="CLOSE_TAG_SPAN" ctype="x-span" equiv-text="&lt;/span&gt;"/></source> <source>Queued <x id="START_TAG_SPAN" ctype="x-span" equiv-text="&lt;span *ngIf=&quot;tasksService.queuedFileTasks.length &gt; 0&quot; class=&quot;badge bg-secondary ms-1&quot;&gt;"/><x id="INTERPOLATION" equiv-text="{{tasksService.queuedFileTasks.length}}"/><x id="CLOSE_TAG_SPAN" ctype="x-span" equiv-text="&lt;/span&gt;"/></source>
@ -3479,7 +3479,7 @@
<context context-type="sourcefile">src/app/components/manage/tasks/tasks.component.html</context> <context context-type="sourcefile">src/app/components/manage/tasks/tasks.component.html</context>
<context context-type="linenumber">114</context> <context context-type="linenumber">114</context>
</context-group> </context-group>
<target state="needs-translation">Queued <x id="START_TAG_SPAN" ctype="x-span" equiv-text="&lt;span *ngIf=&quot;tasksService.queuedFileTasks.length &gt; 0&quot; class=&quot;badge bg-secondary ms-1&quot;&gt;"/><x id="INTERPOLATION" equiv-text="{{tasksService.queuedFileTasks.length}}"/><x id="CLOSE_TAG_SPAN" ctype="x-span" equiv-text="&lt;/span&gt;"/></target> <target state="translated">V čakalni vrsti <x id="START_TAG_SPAN" ctype="x-span" equiv-text="&lt;span *ngIf=&quot;tasksService.queuedFileTasks.length &gt; 0&quot; class=&quot;badge bg-secondary ms-1&quot;&gt;"/><x id="INTERPOLATION" equiv-text="{{tasksService.queuedFileTasks.length}}"/><x id="CLOSE_TAG_SPAN" ctype="x-span" equiv-text="&lt;/span&gt;"/></target>
</trans-unit> </trans-unit>
<trans-unit id="5404910960991552159" datatype="html"> <trans-unit id="5404910960991552159" datatype="html">
<source>Dismiss selected</source> <source>Dismiss selected</source>
@ -3487,7 +3487,7 @@
<context context-type="sourcefile">src/app/components/manage/tasks/tasks.component.ts</context> <context context-type="sourcefile">src/app/components/manage/tasks/tasks.component.ts</context>
<context context-type="linenumber">21</context> <context context-type="linenumber">21</context>
</context-group> </context-group>
<target state="needs-translation">Dismiss selected</target> <target state="translated">Opusti izbrano</target>
</trans-unit> </trans-unit>
<trans-unit id="8829078752502782653" datatype="html"> <trans-unit id="8829078752502782653" datatype="html">
<source>Dismiss all</source> <source>Dismiss all</source>
@ -3499,7 +3499,7 @@
<context context-type="sourcefile">src/app/components/manage/tasks/tasks.component.ts</context> <context context-type="sourcefile">src/app/components/manage/tasks/tasks.component.ts</context>
<context context-type="linenumber">52</context> <context context-type="linenumber">52</context>
</context-group> </context-group>
<target state="needs-translation">Dismiss all</target> <target state="translated">Opusti vse</target>
</trans-unit> </trans-unit>
<trans-unit id="1323591410517879795" datatype="html"> <trans-unit id="1323591410517879795" datatype="html">
<source>Confirm Dismiss All</source> <source>Confirm Dismiss All</source>
@ -3507,7 +3507,7 @@
<context context-type="sourcefile">src/app/components/manage/tasks/tasks.component.ts</context> <context context-type="sourcefile">src/app/components/manage/tasks/tasks.component.ts</context>
<context context-type="linenumber">50</context> <context context-type="linenumber">50</context>
</context-group> </context-group>
<target state="needs-translation">Confirm Dismiss All</target> <target state="translated">Potrdi Opusti vse</target>
</trans-unit> </trans-unit>
<trans-unit id="6566358716882976903" datatype="html"> <trans-unit id="6566358716882976903" datatype="html">
<source>tasks?</source> <source>tasks?</source>
@ -3515,7 +3515,7 @@
<context context-type="sourcefile">src/app/components/manage/tasks/tasks.component.ts</context> <context context-type="sourcefile">src/app/components/manage/tasks/tasks.component.ts</context>
<context context-type="linenumber">52</context> <context context-type="linenumber">52</context>
</context-group> </context-group>
<target state="needs-translation">tasks?</target> <target state="translated">naloge?</target>
</trans-unit> </trans-unit>
<trans-unit id="181464970911903082" datatype="html"> <trans-unit id="181464970911903082" datatype="html">
<source>404 Not Found</source> <source>404 Not Found</source>
@ -3619,7 +3619,7 @@
<context context-type="sourcefile">src/app/guards/dirty-doc.guard.ts</context> <context context-type="sourcefile">src/app/guards/dirty-doc.guard.ts</context>
<context context-type="linenumber">17</context> <context context-type="linenumber">17</context>
</context-group> </context-group>
<target state="needs-translation">Warning: You have unsaved changes to your document(s).</target> <target state="translated">Opozorilo: v dokumentih imate neshranjene spremembe.</target>
</trans-unit> </trans-unit>
<trans-unit id="159901853873315050" datatype="html"> <trans-unit id="159901853873315050" datatype="html">
<source>Unsaved Changes</source> <source>Unsaved Changes</source>
@ -3803,7 +3803,7 @@
<context context-type="sourcefile">src/app/services/open-documents.service.ts</context> <context context-type="sourcefile">src/app/services/open-documents.service.ts</context>
<context context-type="linenumber">118</context> <context context-type="linenumber">118</context>
</context-group> </context-group>
<target state="needs-translation">You have unsaved changes to the document</target> <target state="translated">Imate neshranjene spremembe dokumenta</target>
</trans-unit> </trans-unit>
<trans-unit id="2089045849587358256" datatype="html"> <trans-unit id="2089045849587358256" datatype="html">
<source>Are you sure you want to close this document?</source> <source>Are you sure you want to close this document?</source>
@ -3868,7 +3868,7 @@
<context context-type="sourcefile">src/app/services/settings.service.ts</context> <context context-type="sourcefile">src/app/services/settings.service.ts</context>
<context context-type="linenumber">146</context> <context context-type="linenumber">146</context>
</context-group> </context-group>
<target state="needs-translation">Belarusian</target> <target state="translated">Beloruščina</target>
</trans-unit> </trans-unit>
<trans-unit id="2719780722934172508" datatype="html"> <trans-unit id="2719780722934172508" datatype="html">
<source>Czech</source> <source>Czech</source>
@ -3988,7 +3988,7 @@
<context context-type="sourcefile">src/app/services/settings.service.ts</context> <context context-type="sourcefile">src/app/services/settings.service.ts</context>
<context context-type="linenumber">236</context> <context context-type="linenumber">236</context>
</context-group> </context-group>
<target state="needs-translation">Slovenian</target> <target state="translated">Slovenščina</target>
</trans-unit> </trans-unit>
<trans-unit id="8608389829607915090" datatype="html"> <trans-unit id="8608389829607915090" datatype="html">
<source>Serbian</source> <source>Serbian</source>
@ -3996,7 +3996,7 @@
<context context-type="sourcefile">src/app/services/settings.service.ts</context> <context context-type="sourcefile">src/app/services/settings.service.ts</context>
<context context-type="linenumber">242</context> <context context-type="linenumber">242</context>
</context-group> </context-group>
<target state="needs-translation">Serbian</target> <target state="translated">Srbščina</target>
</trans-unit> </trans-unit>
<trans-unit id="499386805970351976" datatype="html"> <trans-unit id="499386805970351976" datatype="html">
<source>Swedish</source> <source>Swedish</source>
@ -4012,7 +4012,7 @@
<context context-type="sourcefile">src/app/services/settings.service.ts</context> <context context-type="sourcefile">src/app/services/settings.service.ts</context>
<context context-type="linenumber">254</context> <context context-type="linenumber">254</context>
</context-group> </context-group>
<target state="needs-translation">Turkish</target> <target state="translated">Turščina</target>
</trans-unit> </trans-unit>
<trans-unit id="4689443708886954687" datatype="html"> <trans-unit id="4689443708886954687" datatype="html">
<source>Chinese Simplified</source> <source>Chinese Simplified</source>
@ -4020,7 +4020,7 @@
<context context-type="sourcefile">src/app/services/settings.service.ts</context> <context context-type="sourcefile">src/app/services/settings.service.ts</context>
<context context-type="linenumber">260</context> <context context-type="linenumber">260</context>
</context-group> </context-group>
<target state="needs-translation">Chinese Simplified</target> <target state="translated">Poenostavljena kitajščina</target>
</trans-unit> </trans-unit>
<trans-unit id="4912706592792948707" datatype="html"> <trans-unit id="4912706592792948707" datatype="html">
<source>ISO 8601</source> <source>ISO 8601</source>
@ -4036,7 +4036,7 @@
<context context-type="sourcefile">src/app/services/settings.service.ts</context> <context context-type="sourcefile">src/app/services/settings.service.ts</context>
<context context-type="linenumber">372</context> <context context-type="linenumber">372</context>
</context-group> </context-group>
<target state="needs-translation">Successfully completed one-time migratration of settings to the database!</target> <target state="translated">Uspešno opravljena enkratna migracija nastavitev v bazo!</target>
</trans-unit> </trans-unit>
<trans-unit id="5558341108007064934" datatype="html"> <trans-unit id="5558341108007064934" datatype="html">
<source>Unable to migrate settings to the database, please try saving manually.</source> <source>Unable to migrate settings to the database, please try saving manually.</source>
@ -4044,7 +4044,7 @@
<context context-type="sourcefile">src/app/services/settings.service.ts</context> <context context-type="sourcefile">src/app/services/settings.service.ts</context>
<context context-type="linenumber">373</context> <context context-type="linenumber">373</context>
</context-group> </context-group>
<target state="needs-translation">Unable to migrate settings to the database, please try saving manually.</target> <target state="translated">Nastavitev ni mogoče preseliti v bazo podatkov, poskusite jih shraniti ročno.</target>
</trans-unit> </trans-unit>
<trans-unit id="1519954996184640001" datatype="html"> <trans-unit id="1519954996184640001" datatype="html">
<source>Error</source> <source>Error</source>

View File

@ -3,12 +3,15 @@ import os
import shutil import shutil
import tempfile import tempfile
from functools import lru_cache from functools import lru_cache
from typing import List # for type hinting. Can be removed, if only Python >3.8 is used from typing import List
from typing import Optional
from typing import Tuple
import magic import magic
from django.conf import settings from django.conf import settings
from pdf2image import convert_from_path from pikepdf import Page
from pikepdf import Pdf from pikepdf import Pdf
from pikepdf import PdfImage
from PIL import Image from PIL import Image
from PIL import ImageSequence from PIL import ImageSequence
from pyzbar import pyzbar from pyzbar import pyzbar
@ -31,7 +34,7 @@ def supported_file_type(mime_type) -> bool:
return mime_type in supported_mime return mime_type in supported_mime
def barcode_reader(image) -> List[str]: def barcode_reader(image: Image) -> List[str]:
""" """
Read any barcodes contained in image Read any barcodes contained in image
Returns a list containing all found barcodes Returns a list containing all found barcodes
@ -98,21 +101,39 @@ def convert_from_tiff_to_pdf(filepath: str) -> str:
return newpath return newpath
def scan_file_for_separating_barcodes(filepath: str) -> List[int]: def scan_file_for_separating_barcodes(filepath: str) -> Tuple[Optional[str], List[int]]:
""" """
Scan the provided pdf file for page separating barcodes Scan the provided pdf file for page separating barcodes
Returns a list of pagenumbers, which separate the file Returns a PDF filepath and a list of pagenumbers,
which separate the file into new files
""" """
separator_page_numbers = [] separator_page_numbers = []
separator_barcode = str(settings.CONSUMER_BARCODE_STRING) pdf_filepath = None
# use a temporary directory in case the file os too big to handle in memory
with tempfile.TemporaryDirectory() as path: mime_type = get_file_mime_type(filepath)
pages_from_path = convert_from_path(filepath, output_folder=path)
for current_page_number, page in enumerate(pages_from_path): if supported_file_type(mime_type):
current_barcodes = barcode_reader(page) pdf_filepath = filepath
if separator_barcode in current_barcodes: if mime_type == "image/tiff":
separator_page_numbers.append(current_page_number) pdf_filepath = convert_from_tiff_to_pdf(filepath)
return separator_page_numbers
pdf = Pdf.open(pdf_filepath)
for page_num, page in enumerate(pdf.pages):
for image_key in page.images:
pdfimage = PdfImage(page.images[image_key])
pillow_img = pdfimage.as_pil_image()
detected_barcodes = barcode_reader(pillow_img)
if settings.CONSUMER_BARCODE_STRING in detected_barcodes:
separator_page_numbers.append(page_num)
else:
logger.warning(
f"Unsupported file format for barcode reader: {str(mime_type)}",
)
return pdf_filepath, separator_page_numbers
def separate_pages(filepath: str, pages_to_split_on: List[int]) -> List[str]: def separate_pages(filepath: str, pages_to_split_on: List[int]) -> List[str]:
@ -122,47 +143,56 @@ def separate_pages(filepath: str, pages_to_split_on: List[int]) -> List[str]:
Returns a list of (temporary) filepaths to consume. Returns a list of (temporary) filepaths to consume.
These will need to be deleted later. These will need to be deleted later.
""" """
document_paths = []
if not pages_to_split_on:
logger.warning("No pages to split on!")
return document_paths
os.makedirs(settings.SCRATCH_DIR, exist_ok=True) os.makedirs(settings.SCRATCH_DIR, exist_ok=True)
tempdir = tempfile.mkdtemp(prefix="paperless-", dir=settings.SCRATCH_DIR) tempdir = tempfile.mkdtemp(prefix="paperless-", dir=settings.SCRATCH_DIR)
fname = os.path.splitext(os.path.basename(filepath))[0] fname = os.path.splitext(os.path.basename(filepath))[0]
pdf = Pdf.open(filepath) pdf = Pdf.open(filepath)
document_paths = []
logger.debug(f"Temp dir is {str(tempdir)}") # A list of documents, ie a list of lists of pages
if not pages_to_split_on: documents: List[List[Page]] = []
logger.warning("No pages to split on!") # A single document, ie a list of pages
else: document: List[Page] = []
# go from the first page to the first separator page
for idx, page in enumerate(pdf.pages):
# Keep building the new PDF as long as it is not a
# separator index
if idx not in pages_to_split_on:
document.append(page)
# Make sure to append the very last document to the documents
if idx == (len(pdf.pages) - 1):
documents.append(document)
document = []
else:
# This is a split index, save the current PDF pages, and restart
# a new destination page listing
logger.debug(f"Starting new document at idx {idx}")
documents.append(document)
document = []
documents = [x for x in documents if len(x)]
logger.debug(f"Split into {len(documents)} new documents")
# Write the new documents out
for doc_idx, document in enumerate(documents):
dst = Pdf.new() dst = Pdf.new()
for n, page in enumerate(pdf.pages): dst.pages.extend(document)
if n < pages_to_split_on[0]:
dst.pages.append(page) output_filename = f"{fname}_document_{doc_idx}.pdf"
output_filename = f"{fname}_document_0.pdf"
logger.debug(f"pdf no:{doc_idx} has {len(dst.pages)} pages")
savepath = os.path.join(tempdir, output_filename) savepath = os.path.join(tempdir, output_filename)
with open(savepath, "wb") as out: with open(savepath, "wb") as out:
dst.save(out) dst.save(out)
document_paths = [savepath] document_paths.append(savepath)
# iterate through the rest of the document
for count, page_number in enumerate(pages_to_split_on):
logger.debug(f"Count: {str(count)} page_number: {str(page_number)}")
dst = Pdf.new()
try:
next_page = pages_to_split_on[count + 1]
except IndexError:
next_page = len(pdf.pages)
# skip the first page_number. This contains the barcode page
for page in range(page_number + 1, next_page):
logger.debug(
f"page_number: {str(page_number)} next_page: {str(next_page)}",
)
dst.pages.append(pdf.pages[page])
output_filename = f"{fname}_document_{str(count + 1)}.pdf"
logger.debug(f"pdf no:{str(count)} has {str(len(dst.pages))} pages")
savepath = os.path.join(tempdir, output_filename)
with open(savepath, "wb") as out:
dst.save(out)
document_paths.append(savepath)
logger.debug(f"Temp files are {str(document_paths)}")
return document_paths return document_paths

View File

@ -96,29 +96,13 @@ def consume_file(
# check for separators in current document # check for separators in current document
if settings.CONSUMER_ENABLE_BARCODES: if settings.CONSUMER_ENABLE_BARCODES:
mime_type = barcodes.get_file_mime_type(path) pdf_filepath, separators = barcodes.scan_file_for_separating_barcodes(path)
if not barcodes.supported_file_type(mime_type): if separators:
# if not supported, skip this routine logger.debug(
logger.warning( f"Pages with separators found in: {str(path)}",
f"Unsupported file format for barcode reader: {str(mime_type)}",
) )
else: document_list = barcodes.separate_pages(pdf_filepath, separators)
separators = []
document_list = []
if mime_type == "image/tiff":
file_to_process = barcodes.convert_from_tiff_to_pdf(path)
else:
file_to_process = path
separators = barcodes.scan_file_for_separating_barcodes(file_to_process)
if separators:
logger.debug(
f"Pages with separators found in: {str(path)}",
)
document_list = barcodes.separate_pages(file_to_process, separators)
if document_list: if document_list:
for n, document in enumerate(document_list): for n, document in enumerate(document_list):
@ -134,15 +118,13 @@ def consume_file(
target_dir=path.parent, target_dir=path.parent,
) )
# if we got here, the document was successfully split # Delete the PDF file which was split
# and can safely be deleted os.remove(pdf_filepath)
if mime_type == "image/tiff":
# Remove the TIFF converted to PDF file # If the original was a TIFF, remove the original file as well
logger.debug(f"Deleting file {file_to_process}") if str(pdf_filepath) != str(path):
os.unlink(file_to_process) logger.debug(f"Deleting file {path}")
# Remove the original file (new file is saved above) os.unlink(path)
logger.debug(f"Deleting file {path}")
os.unlink(path)
# notify the sender, otherwise the progress bar # notify the sender, otherwise the progress bar
# in the UI stays stuck # in the UI stays stuck

View File

@ -13,22 +13,23 @@ from PIL import Image
class TestBarcode(DirectoriesMixin, TestCase): class TestBarcode(DirectoriesMixin, TestCase):
SAMPLE_DIR = os.path.join(
os.path.dirname(__file__),
"samples",
)
BARCODE_SAMPLE_DIR = os.path.join(SAMPLE_DIR, "barcodes")
def test_barcode_reader(self): def test_barcode_reader(self):
test_file = os.path.join( test_file = os.path.join(self.BARCODE_SAMPLE_DIR, "barcode-39-PATCHT.png")
os.path.dirname(__file__),
"samples",
"barcodes",
"barcode-39-PATCHT.png",
)
img = Image.open(test_file) img = Image.open(test_file)
separator_barcode = str(settings.CONSUMER_BARCODE_STRING) separator_barcode = str(settings.CONSUMER_BARCODE_STRING)
self.assertEqual(barcodes.barcode_reader(img), [separator_barcode]) self.assertEqual(barcodes.barcode_reader(img), [separator_barcode])
def test_barcode_reader2(self): def test_barcode_reader2(self):
test_file = os.path.join( test_file = os.path.join(
os.path.dirname(__file__), self.BARCODE_SAMPLE_DIR,
"samples",
"barcodes",
"patch-code-t.pbm", "patch-code-t.pbm",
) )
img = Image.open(test_file) img = Image.open(test_file)
@ -37,9 +38,7 @@ class TestBarcode(DirectoriesMixin, TestCase):
def test_barcode_reader_distorsion(self): def test_barcode_reader_distorsion(self):
test_file = os.path.join( test_file = os.path.join(
os.path.dirname(__file__), self.BARCODE_SAMPLE_DIR,
"samples",
"barcodes",
"barcode-39-PATCHT-distorsion.png", "barcode-39-PATCHT-distorsion.png",
) )
img = Image.open(test_file) img = Image.open(test_file)
@ -48,9 +47,7 @@ class TestBarcode(DirectoriesMixin, TestCase):
def test_barcode_reader_distorsion2(self): def test_barcode_reader_distorsion2(self):
test_file = os.path.join( test_file = os.path.join(
os.path.dirname(__file__), self.BARCODE_SAMPLE_DIR,
"samples",
"barcodes",
"barcode-39-PATCHT-distorsion2.png", "barcode-39-PATCHT-distorsion2.png",
) )
img = Image.open(test_file) img = Image.open(test_file)
@ -59,9 +56,7 @@ class TestBarcode(DirectoriesMixin, TestCase):
def test_barcode_reader_unreadable(self): def test_barcode_reader_unreadable(self):
test_file = os.path.join( test_file = os.path.join(
os.path.dirname(__file__), self.BARCODE_SAMPLE_DIR,
"samples",
"barcodes",
"barcode-39-PATCHT-unreadable.png", "barcode-39-PATCHT-unreadable.png",
) )
img = Image.open(test_file) img = Image.open(test_file)
@ -69,9 +64,7 @@ class TestBarcode(DirectoriesMixin, TestCase):
def test_barcode_reader_qr(self): def test_barcode_reader_qr(self):
test_file = os.path.join( test_file = os.path.join(
os.path.dirname(__file__), self.BARCODE_SAMPLE_DIR,
"samples",
"barcodes",
"qr-code-PATCHT.png", "qr-code-PATCHT.png",
) )
img = Image.open(test_file) img = Image.open(test_file)
@ -80,9 +73,7 @@ class TestBarcode(DirectoriesMixin, TestCase):
def test_barcode_reader_128(self): def test_barcode_reader_128(self):
test_file = os.path.join( test_file = os.path.join(
os.path.dirname(__file__), self.BARCODE_SAMPLE_DIR,
"samples",
"barcodes",
"barcode-128-PATCHT.png", "barcode-128-PATCHT.png",
) )
img = Image.open(test_file) img = Image.open(test_file)
@ -90,15 +81,13 @@ class TestBarcode(DirectoriesMixin, TestCase):
self.assertEqual(barcodes.barcode_reader(img), [separator_barcode]) self.assertEqual(barcodes.barcode_reader(img), [separator_barcode])
def test_barcode_reader_no_barcode(self): def test_barcode_reader_no_barcode(self):
test_file = os.path.join(os.path.dirname(__file__), "samples", "simple.png") test_file = os.path.join(self.SAMPLE_DIR, "simple.png")
img = Image.open(test_file) img = Image.open(test_file)
self.assertEqual(barcodes.barcode_reader(img), []) self.assertEqual(barcodes.barcode_reader(img), [])
def test_barcode_reader_custom_separator(self): def test_barcode_reader_custom_separator(self):
test_file = os.path.join( test_file = os.path.join(
os.path.dirname(__file__), self.BARCODE_SAMPLE_DIR,
"samples",
"barcodes",
"barcode-39-custom.png", "barcode-39-custom.png",
) )
img = Image.open(test_file) img = Image.open(test_file)
@ -106,9 +95,7 @@ class TestBarcode(DirectoriesMixin, TestCase):
def test_barcode_reader_custom_qr_separator(self): def test_barcode_reader_custom_qr_separator(self):
test_file = os.path.join( test_file = os.path.join(
os.path.dirname(__file__), self.BARCODE_SAMPLE_DIR,
"samples",
"barcodes",
"barcode-qr-custom.png", "barcode-qr-custom.png",
) )
img = Image.open(test_file) img = Image.open(test_file)
@ -116,9 +103,7 @@ class TestBarcode(DirectoriesMixin, TestCase):
def test_barcode_reader_custom_128_separator(self): def test_barcode_reader_custom_128_separator(self):
test_file = os.path.join( test_file = os.path.join(
os.path.dirname(__file__), self.BARCODE_SAMPLE_DIR,
"samples",
"barcodes",
"barcode-128-custom.png", "barcode-128-custom.png",
) )
img = Image.open(test_file) img = Image.open(test_file)
@ -126,19 +111,15 @@ class TestBarcode(DirectoriesMixin, TestCase):
def test_get_mime_type(self): def test_get_mime_type(self):
tiff_file = os.path.join( tiff_file = os.path.join(
os.path.dirname(__file__), self.SAMPLE_DIR,
"samples",
"simple.tiff", "simple.tiff",
) )
pdf_file = os.path.join( pdf_file = os.path.join(
os.path.dirname(__file__), self.SAMPLE_DIR,
"samples",
"simple.pdf", "simple.pdf",
) )
png_file = os.path.join( png_file = os.path.join(
os.path.dirname(__file__), self.BARCODE_SAMPLE_DIR,
"samples",
"barcodes",
"barcode-128-custom.png", "barcode-128-custom.png",
) )
tiff_file_no_extension = os.path.join(settings.SCRATCH_DIR, "testfile1") tiff_file_no_extension = os.path.join(settings.SCRATCH_DIR, "testfile1")
@ -173,8 +154,7 @@ class TestBarcode(DirectoriesMixin, TestCase):
def test_convert_error_from_pdf_to_pdf(self): def test_convert_error_from_pdf_to_pdf(self):
test_file = os.path.join( test_file = os.path.join(
os.path.dirname(__file__), self.SAMPLE_DIR,
"samples",
"simple.pdf", "simple.pdf",
) )
dst = os.path.join(settings.SCRATCH_DIR, "simple.pdf") dst = os.path.join(settings.SCRATCH_DIR, "simple.pdf")
@ -183,117 +163,155 @@ class TestBarcode(DirectoriesMixin, TestCase):
def test_scan_file_for_separating_barcodes(self): def test_scan_file_for_separating_barcodes(self):
test_file = os.path.join( test_file = os.path.join(
os.path.dirname(__file__), self.BARCODE_SAMPLE_DIR,
"samples",
"barcodes",
"patch-code-t.pdf", "patch-code-t.pdf",
) )
pages = barcodes.scan_file_for_separating_barcodes(test_file) pdf_file, separator_page_numbers = barcodes.scan_file_for_separating_barcodes(
self.assertEqual(pages, [0]) test_file,
)
self.assertEqual(pdf_file, test_file)
self.assertListEqual(separator_page_numbers, [0])
def test_scan_file_for_separating_barcodes2(self): def test_scan_file_for_separating_barcodes2(self):
test_file = os.path.join(os.path.dirname(__file__), "samples", "simple.pdf") test_file = os.path.join(self.SAMPLE_DIR, "simple.pdf")
pages = barcodes.scan_file_for_separating_barcodes(test_file) pdf_file, separator_page_numbers = barcodes.scan_file_for_separating_barcodes(
self.assertEqual(pages, []) test_file,
)
self.assertEqual(pdf_file, test_file)
self.assertListEqual(separator_page_numbers, [])
def test_scan_file_for_separating_barcodes3(self): def test_scan_file_for_separating_barcodes3(self):
test_file = os.path.join( test_file = os.path.join(
os.path.dirname(__file__), self.BARCODE_SAMPLE_DIR,
"samples",
"barcodes",
"patch-code-t-middle.pdf", "patch-code-t-middle.pdf",
) )
pages = barcodes.scan_file_for_separating_barcodes(test_file) pdf_file, separator_page_numbers = barcodes.scan_file_for_separating_barcodes(
self.assertEqual(pages, [1]) test_file,
)
self.assertEqual(pdf_file, test_file)
self.assertListEqual(separator_page_numbers, [1])
def test_scan_file_for_separating_barcodes4(self): def test_scan_file_for_separating_barcodes4(self):
test_file = os.path.join( test_file = os.path.join(
os.path.dirname(__file__), self.BARCODE_SAMPLE_DIR,
"samples",
"barcodes",
"several-patcht-codes.pdf", "several-patcht-codes.pdf",
) )
pages = barcodes.scan_file_for_separating_barcodes(test_file) pdf_file, separator_page_numbers = barcodes.scan_file_for_separating_barcodes(
self.assertEqual(pages, [2, 5]) test_file,
)
self.assertEqual(pdf_file, test_file)
self.assertListEqual(separator_page_numbers, [2, 5])
def test_scan_file_for_separating_barcodes_upsidedown(self): def test_scan_file_for_separating_barcodes_upsidedown(self):
test_file = os.path.join( test_file = os.path.join(
os.path.dirname(__file__), self.BARCODE_SAMPLE_DIR,
"samples",
"barcodes",
"patch-code-t-middle_reverse.pdf", "patch-code-t-middle_reverse.pdf",
) )
pages = barcodes.scan_file_for_separating_barcodes(test_file) pdf_file, separator_page_numbers = barcodes.scan_file_for_separating_barcodes(
self.assertEqual(pages, [1]) test_file,
)
self.assertEqual(pdf_file, test_file)
self.assertListEqual(separator_page_numbers, [1])
def test_scan_file_for_separating_qr_barcodes(self): def test_scan_file_for_separating_qr_barcodes(self):
test_file = os.path.join( test_file = os.path.join(
os.path.dirname(__file__), self.BARCODE_SAMPLE_DIR,
"samples",
"barcodes",
"patch-code-t-qr.pdf", "patch-code-t-qr.pdf",
) )
pages = barcodes.scan_file_for_separating_barcodes(test_file) pdf_file, separator_page_numbers = barcodes.scan_file_for_separating_barcodes(
self.assertEqual(pages, [0]) test_file,
)
self.assertEqual(pdf_file, test_file)
self.assertListEqual(separator_page_numbers, [0])
@override_settings(CONSUMER_BARCODE_STRING="CUSTOM BARCODE") @override_settings(CONSUMER_BARCODE_STRING="CUSTOM BARCODE")
def test_scan_file_for_separating_custom_barcodes(self): def test_scan_file_for_separating_custom_barcodes(self):
test_file = os.path.join( test_file = os.path.join(
os.path.dirname(__file__), self.BARCODE_SAMPLE_DIR,
"samples",
"barcodes",
"barcode-39-custom.pdf", "barcode-39-custom.pdf",
) )
pages = barcodes.scan_file_for_separating_barcodes(test_file) pdf_file, separator_page_numbers = barcodes.scan_file_for_separating_barcodes(
self.assertEqual(pages, [0]) test_file,
)
self.assertEqual(pdf_file, test_file)
self.assertListEqual(separator_page_numbers, [0])
@override_settings(CONSUMER_BARCODE_STRING="CUSTOM BARCODE") @override_settings(CONSUMER_BARCODE_STRING="CUSTOM BARCODE")
def test_scan_file_for_separating_custom_qr_barcodes(self): def test_scan_file_for_separating_custom_qr_barcodes(self):
test_file = os.path.join( test_file = os.path.join(
os.path.dirname(__file__), self.BARCODE_SAMPLE_DIR,
"samples",
"barcodes",
"barcode-qr-custom.pdf", "barcode-qr-custom.pdf",
) )
pages = barcodes.scan_file_for_separating_barcodes(test_file) pdf_file, separator_page_numbers = barcodes.scan_file_for_separating_barcodes(
self.assertEqual(pages, [0]) test_file,
)
self.assertEqual(pdf_file, test_file)
self.assertListEqual(separator_page_numbers, [0])
@override_settings(CONSUMER_BARCODE_STRING="CUSTOM BARCODE") @override_settings(CONSUMER_BARCODE_STRING="CUSTOM BARCODE")
def test_scan_file_for_separating_custom_128_barcodes(self): def test_scan_file_for_separating_custom_128_barcodes(self):
test_file = os.path.join( test_file = os.path.join(
os.path.dirname(__file__), self.BARCODE_SAMPLE_DIR,
"samples",
"barcodes",
"barcode-128-custom.pdf", "barcode-128-custom.pdf",
) )
pages = barcodes.scan_file_for_separating_barcodes(test_file) pdf_file, separator_page_numbers = barcodes.scan_file_for_separating_barcodes(
self.assertEqual(pages, [0]) test_file,
)
self.assertEqual(pdf_file, test_file)
self.assertListEqual(separator_page_numbers, [0])
def test_scan_file_for_separating_wrong_qr_barcodes(self): def test_scan_file_for_separating_wrong_qr_barcodes(self):
test_file = os.path.join( test_file = os.path.join(
os.path.dirname(__file__), self.BARCODE_SAMPLE_DIR,
"samples",
"barcodes",
"barcode-39-custom.pdf", "barcode-39-custom.pdf",
) )
pages = barcodes.scan_file_for_separating_barcodes(test_file) pdf_file, separator_page_numbers = barcodes.scan_file_for_separating_barcodes(
self.assertEqual(pages, []) test_file,
)
self.assertEqual(pdf_file, test_file)
self.assertListEqual(separator_page_numbers, [])
def test_separate_pages(self): def test_separate_pages(self):
test_file = os.path.join(
self.BARCODE_SAMPLE_DIR,
"patch-code-t-middle.pdf",
)
pages = barcodes.separate_pages(test_file, [1])
self.assertEqual(len(pages), 2)
def test_separate_pages_double_code(self):
"""
GIVEN:
- Input PDF with two patch code pages in a row
WHEN:
- The input file is split
THEN:
- Only two files are output
"""
test_file = os.path.join( test_file = os.path.join(
os.path.dirname(__file__), os.path.dirname(__file__),
"samples", "samples",
"barcodes", "barcodes",
"patch-code-t-middle.pdf", "patch-code-t-double.pdf",
) )
pages = barcodes.separate_pages(test_file, [1]) pages = barcodes.separate_pages(test_file, [1, 2])
self.assertEqual(len(pages), 2) self.assertEqual(len(pages), 2)
def test_separate_pages_no_list(self): def test_separate_pages_no_list(self):
test_file = os.path.join( test_file = os.path.join(
os.path.dirname(__file__), self.BARCODE_SAMPLE_DIR,
"samples",
"barcodes",
"patch-code-t-middle.pdf", "patch-code-t-middle.pdf",
) )
with self.assertLogs("paperless.barcodes", level="WARNING") as cm: with self.assertLogs("paperless.barcodes", level="WARNING") as cm:
@ -308,9 +326,7 @@ class TestBarcode(DirectoriesMixin, TestCase):
def test_save_to_dir(self): def test_save_to_dir(self):
test_file = os.path.join( test_file = os.path.join(
os.path.dirname(__file__), self.BARCODE_SAMPLE_DIR,
"samples",
"barcodes",
"patch-code-t.pdf", "patch-code-t.pdf",
) )
tempdir = tempfile.mkdtemp(prefix="paperless-", dir=settings.SCRATCH_DIR) tempdir = tempfile.mkdtemp(prefix="paperless-", dir=settings.SCRATCH_DIR)
@ -320,9 +336,7 @@ class TestBarcode(DirectoriesMixin, TestCase):
def test_save_to_dir2(self): def test_save_to_dir2(self):
test_file = os.path.join( test_file = os.path.join(
os.path.dirname(__file__), self.BARCODE_SAMPLE_DIR,
"samples",
"barcodes",
"patch-code-t.pdf", "patch-code-t.pdf",
) )
nonexistingdir = "/nowhere" nonexistingdir = "/nowhere"
@ -340,9 +354,7 @@ class TestBarcode(DirectoriesMixin, TestCase):
def test_save_to_dir3(self): def test_save_to_dir3(self):
test_file = os.path.join( test_file = os.path.join(
os.path.dirname(__file__), self.BARCODE_SAMPLE_DIR,
"samples",
"barcodes",
"patch-code-t.pdf", "patch-code-t.pdf",
) )
tempdir = tempfile.mkdtemp(prefix="paperless-", dir=settings.SCRATCH_DIR) tempdir = tempfile.mkdtemp(prefix="paperless-", dir=settings.SCRATCH_DIR)
@ -352,31 +364,36 @@ class TestBarcode(DirectoriesMixin, TestCase):
def test_barcode_splitter(self): def test_barcode_splitter(self):
test_file = os.path.join( test_file = os.path.join(
os.path.dirname(__file__), self.BARCODE_SAMPLE_DIR,
"samples",
"barcodes",
"patch-code-t-middle.pdf", "patch-code-t-middle.pdf",
) )
tempdir = tempfile.mkdtemp(prefix="paperless-", dir=settings.SCRATCH_DIR) tempdir = tempfile.mkdtemp(prefix="paperless-", dir=settings.SCRATCH_DIR)
separators = barcodes.scan_file_for_separating_barcodes(test_file)
self.assertTrue(separators) pdf_file, separator_page_numbers = barcodes.scan_file_for_separating_barcodes(
document_list = barcodes.separate_pages(test_file, separators) test_file,
)
self.assertEqual(test_file, pdf_file)
self.assertTrue(len(separator_page_numbers) > 0)
document_list = barcodes.separate_pages(test_file, separator_page_numbers)
self.assertTrue(document_list) self.assertTrue(document_list)
for document in document_list: for document in document_list:
barcodes.save_to_dir(document, target_dir=tempdir) barcodes.save_to_dir(document, target_dir=tempdir)
target_file1 = os.path.join(tempdir, "patch-code-t-middle_document_0.pdf") target_file1 = os.path.join(tempdir, "patch-code-t-middle_document_0.pdf")
target_file2 = os.path.join(tempdir, "patch-code-t-middle_document_1.pdf") target_file2 = os.path.join(tempdir, "patch-code-t-middle_document_1.pdf")
self.assertTrue(os.path.isfile(target_file1)) self.assertTrue(os.path.isfile(target_file1))
self.assertTrue(os.path.isfile(target_file2)) self.assertTrue(os.path.isfile(target_file2))
@override_settings(CONSUMER_ENABLE_BARCODES=True) @override_settings(CONSUMER_ENABLE_BARCODES=True)
def test_consume_barcode_file(self): def test_consume_barcode_file(self):
test_file = os.path.join( test_file = os.path.join(
os.path.dirname(__file__), self.BARCODE_SAMPLE_DIR,
"samples",
"barcodes",
"patch-code-t-middle.pdf", "patch-code-t-middle.pdf",
) )
dst = os.path.join(settings.SCRATCH_DIR, "patch-code-t-middle.pdf") dst = os.path.join(settings.SCRATCH_DIR, "patch-code-t-middle.pdf")
shutil.copy(test_file, dst) shutil.copy(test_file, dst)
@ -388,9 +405,7 @@ class TestBarcode(DirectoriesMixin, TestCase):
) )
def test_consume_barcode_tiff_file(self): def test_consume_barcode_tiff_file(self):
test_file = os.path.join( test_file = os.path.join(
os.path.dirname(__file__), self.BARCODE_SAMPLE_DIR,
"samples",
"barcodes",
"patch-code-t-middle.tiff", "patch-code-t-middle.tiff",
) )
dst = os.path.join(settings.SCRATCH_DIR, "patch-code-t-middle.tiff") dst = os.path.join(settings.SCRATCH_DIR, "patch-code-t-middle.tiff")
@ -412,18 +427,17 @@ class TestBarcode(DirectoriesMixin, TestCase):
and continue archiving the file as is. and continue archiving the file as is.
""" """
test_file = os.path.join( test_file = os.path.join(
os.path.dirname(__file__), self.SAMPLE_DIR,
"samples",
"simple.jpg", "simple.jpg",
) )
dst = os.path.join(settings.SCRATCH_DIR, "simple.jpg") dst = os.path.join(settings.SCRATCH_DIR, "simple.jpg")
shutil.copy(test_file, dst) shutil.copy(test_file, dst)
with self.assertLogs("paperless.tasks", level="WARNING") as cm: with self.assertLogs("paperless.barcodes", level="WARNING") as cm:
self.assertIn("Success", tasks.consume_file(dst)) self.assertIn("Success", tasks.consume_file(dst))
self.assertListEqual( self.assertListEqual(
cm.output, cm.output,
[ [
"WARNING:paperless.tasks:Unsupported file format for barcode reader: image/jpeg", "WARNING:paperless.barcodes:Unsupported file format for barcode reader: image/jpeg",
], ],
) )
m.assert_called_once() m.assert_called_once()
@ -445,9 +459,7 @@ class TestBarcode(DirectoriesMixin, TestCase):
the user uploads a supported image file, but without extension the user uploads a supported image file, but without extension
""" """
test_file = os.path.join( test_file = os.path.join(
os.path.dirname(__file__), self.BARCODE_SAMPLE_DIR,
"samples",
"barcodes",
"patch-code-t-middle.tiff", "patch-code-t-middle.tiff",
) )
dst = os.path.join(settings.SCRATCH_DIR, "patch-code-t-middle") dst = os.path.join(settings.SCRATCH_DIR, "patch-code-t-middle")

View File

@ -1,4 +1,6 @@
import grp
import os import os
import pwd
import shutil import shutil
import stat import stat
@ -32,12 +34,15 @@ def path_check(var, directory):
with open(test_file, "w"): with open(test_file, "w"):
pass pass
except PermissionError: except PermissionError:
dir_stat = os.stat(directory)
dir_mode = stat.filemode(dir_stat.st_mode)
dir_owner = pwd.getpwuid(dir_stat.st_uid).pw_name
dir_group = grp.getgrgid(dir_stat.st_gid).gr_name
messages.append( messages.append(
Error( Error(
writeable_message.format(var), writeable_message.format(var),
writeable_hint.format( writeable_hint.format(
f"\n{stat.filemode(os.stat(directory).st_mode)} " f"\n{dir_mode} {dir_owner} {dir_group} " f"{directory}\n",
f"{directory}\n",
), ),
), ),
) )

View File

@ -1,7 +1,7 @@
from typing import Final from typing import Final
from typing import Tuple from typing import Tuple
__version__: Final[Tuple[int, int, int]] = (1, 8, 0) __version__: Final[Tuple[int, int, int]] = (1, 9, 0)
# Version string like X.Y.Z # Version string like X.Y.Z
__full_version_str__: Final[str] = ".".join(map(str, __version__)) __full_version_str__: Final[str] = ".".join(map(str, __version__))
# Version string like X.Y # Version string like X.Y

View File

@ -1,4 +1,5 @@
import os import os
from pathlib import Path
import dateutil.parser import dateutil.parser
import requests import requests
@ -28,6 +29,11 @@ class TikaDocumentParser(DocumentParser):
def extract_metadata(self, document_path, mime_type): def extract_metadata(self, document_path, mime_type):
tika_server = settings.TIKA_ENDPOINT tika_server = settings.TIKA_ENDPOINT
# tika does not support a PathLike, only strings
# ensure this is a string
document_path = str(document_path)
try: try:
parsed = parser.from_file(document_path, tika_server) parsed = parser.from_file(document_path, tika_server)
except Exception as e: except Exception as e:
@ -47,10 +53,14 @@ class TikaDocumentParser(DocumentParser):
for key in parsed["metadata"] for key in parsed["metadata"]
] ]
def parse(self, document_path, mime_type, file_name=None): def parse(self, document_path: Path, mime_type, file_name=None):
self.log("info", f"Sending {document_path} to Tika server") self.log("info", f"Sending {document_path} to Tika server")
tika_server = settings.TIKA_ENDPOINT tika_server = settings.TIKA_ENDPOINT
# tika does not support a PathLike, only strings
# ensure this is a string
document_path = str(document_path)
try: try:
parsed = parser.from_file(document_path, tika_server) parsed = parser.from_file(document_path, tika_server)
except Exception as err: except Exception as err: