mirror of
https://github.com/ONLYOFFICE/document-server-integration.git
synced 2026-03-14 09:42:13 +08:00
Compare commits
37 Commits
v9.0.0.40
...
feature/py
| Author | SHA1 | Date | |
|---|---|---|---|
| e1e9ad04a3 | |||
| 865d071203 | |||
| db48ff64e5 | |||
| 89a6b94178 | |||
| 5ea6d81155 | |||
| 72ea75ec02 | |||
| 89d6917a58 | |||
| d0354c88f5 | |||
| e0ccbd4f93 | |||
| f8194eb459 | |||
| 01f35c9ee4 | |||
| 360c26f7f5 | |||
| c4c621d1f2 | |||
| 14e4904adb | |||
| 69d74d6e8a | |||
| 00a172f9f2 | |||
| 7a4610ce83 | |||
| 8782430bc6 | |||
| be8b83929e | |||
| 2658bdc783 | |||
| d9c9bd6dc3 | |||
| 561117433c | |||
| 0c8cb2becf | |||
| dce9b7911d | |||
| ae8b32090f | |||
| 82c378f3c3 | |||
| 5bcc8c0f16 | |||
| 478784fbbd | |||
| fb18fc501d | |||
| b5941803ee | |||
| 50e5b6e0f5 | |||
| e79dcfbf59 | |||
| 58855f0cbe | |||
| c159ff68ae | |||
| 88e0411c56 | |||
| 13be815042 | |||
| ccdf599b45 |
3
web/documentserver-example/python/.gitignore
vendored
Normal file
3
web/documentserver-example/python/.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
*.egg-info
|
||||
build
|
||||
storage/*
|
||||
1
web/documentserver-example/python/.python-version
Normal file
1
web/documentserver-example/python/.python-version
Normal file
@ -0,0 +1 @@
|
||||
3.11
|
||||
@ -28,10 +28,18 @@ jQuery.UI - jQuery UI is an open source library of interface components —
|
||||
License: MIT
|
||||
License File: jQuery.UI.license
|
||||
|
||||
mypy - Optional static typing for Python. (https://github.com/python/mypy/raw/v1.4.1/LICENSE)
|
||||
License: MIT
|
||||
License File: mypy.license
|
||||
|
||||
PyJWT - A Python implementation of RFC 7519. (https://github.com/jpadilla/pyjwt/blob/master/LICENSE)
|
||||
License: MIT
|
||||
License File: PyJWT.license
|
||||
|
||||
pylint - It's not just a linter that annoys you! (https://github.com/pylint-dev/pylint/raw/v2.17.4/LICENSE)
|
||||
License: GPL v2
|
||||
License File: pylint.license
|
||||
|
||||
python-magic - python-magic is a Python interface to the libmagic file type identification library. (https://github.com/ahupp/python-magic/blob/master/LICENSE)
|
||||
License: MIT
|
||||
License File: python-magic.license
|
||||
|
||||
13
web/documentserver-example/python/Dockerfile
Normal file
13
web/documentserver-example/python/Dockerfile
Normal file
@ -0,0 +1,13 @@
|
||||
FROM python:3.11.4-alpine3.18 as example
|
||||
WORKDIR /srv
|
||||
COPY . .
|
||||
RUN \
|
||||
apk update && \
|
||||
apk add --no-cache \
|
||||
libmagic \
|
||||
make && \
|
||||
make prod
|
||||
CMD ["make", "prod-server"]
|
||||
|
||||
FROM nginx:1.23.4-alpine3.17 as proxy
|
||||
COPY proxy/nginx.conf /etc/nginx/nginx.conf
|
||||
39
web/documentserver-example/python/Makefile
Normal file
39
web/documentserver-example/python/Makefile
Normal file
@ -0,0 +1,39 @@
|
||||
.DEFAULT_GOAL := help
|
||||
|
||||
.PHONY: help
|
||||
help: # Show help message for each of the Makefile recipes.
|
||||
@grep -E "^[a-z-]+: #" $(MAKEFILE_LIST) | \
|
||||
sort | \
|
||||
awk 'BEGIN {FS = ": # "}; {printf "%s: %s\n", $$1, $$2}'
|
||||
|
||||
.PHONY: dev
|
||||
dev: # Install development dependencies.
|
||||
@pip install --editable .[development]
|
||||
|
||||
.PHONY: dev-server
|
||||
dev-server: \
|
||||
export DEBUG := true
|
||||
dev-server: # Start the development server on localhost at $PORT (default: 8000).
|
||||
@python manage.py runserver
|
||||
|
||||
.PHONY: lint
|
||||
lint: # Lint the source code for style and check for types.
|
||||
@pylint --recursive=y .
|
||||
@mypy .
|
||||
|
||||
.PHONY: prod
|
||||
prod: # Install production dependencies.
|
||||
@pip install .
|
||||
|
||||
.PHONY: prod-server
|
||||
prod-server: # Start the production server on 0.0.0.0 at $PORT (default: 8000).
|
||||
@python manage.py runserver
|
||||
|
||||
.PHONY: test
|
||||
test: # Recursively run the tests.
|
||||
@python -m unittest ./src/**/*_tests.py
|
||||
|
||||
.PHONY: up
|
||||
up: # Build and up docker containers.
|
||||
@docker-compose build
|
||||
@docker-compose up -d
|
||||
@ -1,108 +0,0 @@
|
||||
import os
|
||||
|
||||
VERSION = '1.5.1'
|
||||
|
||||
FILE_SIZE_MAX = 5242880
|
||||
STORAGE_PATH = 'app_data'
|
||||
|
||||
DOC_SERV_FILLFORMS = [".docx", ".oform"]
|
||||
DOC_SERV_VIEWED = [".djvu", ".oxps", ".pdf", ".xps"] # file extensions that can be viewed
|
||||
DOC_SERV_EDITED = [ # file extensions that can be edited
|
||||
".csv", ".docm", ".docx", ".docxf", ".dotm", ".dotx",
|
||||
".epub", ".fb2", ".html", ".odp", ".ods", ".odt", ".otp",
|
||||
".ots", ".ott", ".potm", ".potx", ".ppsm", ".ppsx", ".pptm",
|
||||
".pptx", ".rtf", ".txt", ".xlsm", ".xlsx", ".xltm", ".xltx"
|
||||
]
|
||||
DOC_SERV_CONVERT = [ # file extensions that can be converted
|
||||
".doc", ".dot", ".dps", ".dpt", ".epub", ".et", ".ett", ".fb2",
|
||||
".fodp", ".fods", ".fodt", ".htm", ".html", ".mht", ".mhtml",
|
||||
".odp", ".ods", ".odt", ".otp", ".ots", ".ott", ".pot", ".pps",
|
||||
".ppt", ".rtf", ".stw", ".sxc", ".sxi", ".sxw", ".wps", ".wpt",
|
||||
".xls", ".xlsb", ".xlt", ".xml"
|
||||
]
|
||||
|
||||
DOC_SERV_TIMEOUT = 120000
|
||||
|
||||
DOC_SERV_SITE_URL = 'http://documentserver/'
|
||||
|
||||
DOC_SERV_CONVERTER_URL = 'ConvertService.ashx'
|
||||
DOC_SERV_API_URL = 'web-apps/apps/api/documents/api.js'
|
||||
DOC_SERV_PRELOADER_URL = 'web-apps/apps/api/documents/cache-scripts.html'
|
||||
DOC_SERV_COMMAND_URL='coauthoring/CommandService.ashx'
|
||||
|
||||
EXAMPLE_DOMAIN = None
|
||||
|
||||
DOC_SERV_JWT_SECRET = '' # the secret key for generating token
|
||||
DOC_SERV_JWT_HEADER = 'Authorization'
|
||||
DOC_SERV_JWT_USE_FOR_REQUEST = True
|
||||
|
||||
DOC_SERV_VERIFY_PEER = False
|
||||
|
||||
EXT_SPREADSHEET = [
|
||||
".xls", ".xlsx", ".xlsm", ".xlsb",
|
||||
".xlt", ".xltx", ".xltm",
|
||||
".ods", ".fods", ".ots", ".csv"
|
||||
]
|
||||
|
||||
EXT_PRESENTATION = [
|
||||
".pps", ".ppsx", ".ppsm",
|
||||
".ppt", ".pptx", ".pptm",
|
||||
".pot", ".potx", ".potm",
|
||||
".odp", ".fodp", ".otp"
|
||||
]
|
||||
|
||||
EXT_DOCUMENT = [
|
||||
".doc", ".docx", ".docm",
|
||||
".dot", ".dotx", ".dotm",
|
||||
".odt", ".fodt", ".ott", ".rtf", ".txt",
|
||||
".html", ".htm", ".mht", ".xml",
|
||||
".pdf", ".djvu", ".fb2", ".epub", ".xps", ".oxps", ".oform"
|
||||
]
|
||||
|
||||
LANGUAGES = {
|
||||
'en': 'English',
|
||||
'hy': 'Armenian',
|
||||
'az': 'Azerbaijani',
|
||||
'eu': 'Basque',
|
||||
'be': 'Belarusian',
|
||||
'bg': 'Bulgarian',
|
||||
'ca': 'Catalan',
|
||||
'zh': 'Chinese (Simplified)',
|
||||
'zh-TW': 'Chinese (Traditional)',
|
||||
'cs': 'Czech',
|
||||
'da': 'Danish',
|
||||
'nl': 'Dutch',
|
||||
'fi': 'Finnish',
|
||||
'fr': 'French',
|
||||
'gl': 'Galego',
|
||||
'de': 'German',
|
||||
'el': 'Greek',
|
||||
'hu': 'Hungarian',
|
||||
'id': 'Indonesian',
|
||||
'it': 'Italian',
|
||||
'ja': 'Japanese',
|
||||
'ko': 'Korean',
|
||||
'lo': 'Lao',
|
||||
'lv': 'Latvian',
|
||||
'ms': 'Malay (Malaysia)',
|
||||
'no': 'Norwegian',
|
||||
'pl': 'Polish',
|
||||
'pt': 'Portuguese (Brazil)',
|
||||
'pt-PT': 'Portuguese (Portugal)',
|
||||
'ro': 'Romanian',
|
||||
'ru': 'Russian',
|
||||
'si': 'Sinhala (Sri Lanka)',
|
||||
'sk': 'Slovak',
|
||||
'sl': 'Slovenian',
|
||||
'es': 'Spanish',
|
||||
'sv': 'Swedish',
|
||||
'tr': 'Turkish',
|
||||
'uk': 'Ukrainian',
|
||||
'vi': 'Vietnamese',
|
||||
'aa-AA': 'Test Language'
|
||||
}
|
||||
|
||||
if os.environ.get("EXAMPLE_DOMAIN"): # generates a link for example domain
|
||||
EXAMPLE_DOMAIN = os.environ.get("EXAMPLE_DOMAIN")
|
||||
if os.environ.get("DOC_SERV"): # generates links for document server
|
||||
DOC_SERV_SITE_URL = os.environ.get("DOC_SERV")
|
||||
40
web/documentserver-example/python/docker-compose.yml
Normal file
40
web/documentserver-example/python/docker-compose.yml
Normal file
@ -0,0 +1,40 @@
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
document-server:
|
||||
container_name: document-server
|
||||
image: onlyoffice/documentserver:7.3.3.50
|
||||
expose:
|
||||
- "80"
|
||||
environment:
|
||||
- JWT_SECRET=your-256-bit-secret
|
||||
|
||||
example:
|
||||
container_name: example
|
||||
build:
|
||||
context: .
|
||||
target: example
|
||||
volumes:
|
||||
- static:/srv/static
|
||||
expose:
|
||||
- "80"
|
||||
environment:
|
||||
- DOCUMENT_SERVER_PRIVATE_URL=http://proxy:3000/
|
||||
- DOCUMENT_SERVER_PUBLIC_URL=http://localhost:3000/
|
||||
- EXAMPLE_URL=http://proxy:8080/
|
||||
- JWT_SECRET=your-256-bit-secret
|
||||
- PORT=80
|
||||
|
||||
proxy:
|
||||
container_name: proxy
|
||||
build:
|
||||
context: .
|
||||
target: proxy
|
||||
volumes:
|
||||
- static:/srv/static
|
||||
ports:
|
||||
- "8080:8080"
|
||||
- "3000:3000"
|
||||
|
||||
volumes:
|
||||
static:
|
||||
@ -28,10 +28,18 @@ jQuery.UI - jQuery UI is an open source library of interface components —
|
||||
License: MIT
|
||||
License File: jQuery.UI.license
|
||||
|
||||
mypy - Optional static typing for Python. (https://github.com/python/mypy/raw/v1.4.1/LICENSE)
|
||||
License: MIT
|
||||
License File: mypy.license
|
||||
|
||||
PyJWT - A Python implementation of RFC 7519. (https://github.com/jpadilla/pyjwt/blob/master/LICENSE)
|
||||
License: MIT
|
||||
License File: PyJWT.license
|
||||
|
||||
pylint - It's not just a linter that annoys you! (https://github.com/pylint-dev/pylint/raw/v2.17.4/LICENSE)
|
||||
License: GPL v2
|
||||
License File: pylint.license
|
||||
|
||||
python-magic - python-magic is a Python interface to the libmagic file type identification library. (https://github.com/ahupp/python-magic/blob/master/LICENSE)
|
||||
License: MIT
|
||||
License File: python-magic.license
|
||||
|
||||
229
web/documentserver-example/python/licenses/mypy.license
Normal file
229
web/documentserver-example/python/licenses/mypy.license
Normal file
@ -0,0 +1,229 @@
|
||||
Mypy (and mypyc) are licensed under the terms of the MIT license, reproduced below.
|
||||
|
||||
= = = = =
|
||||
|
||||
The MIT License
|
||||
|
||||
Copyright (c) 2012-2022 Jukka Lehtosalo and contributors
|
||||
Copyright (c) 2015-2022 Dropbox, Inc.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
to deal in the Software without restriction, including without limitation
|
||||
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
|
||||
= = = = =
|
||||
|
||||
Portions of mypy and mypyc are licensed under different licenses.
|
||||
The files
|
||||
mypyc/lib-rt/pythonsupport.h, mypyc/lib-rt/getargs.c and
|
||||
mypyc/lib-rt/getargsfast.c are licensed under the PSF 2 License, reproduced
|
||||
below.
|
||||
|
||||
= = = = =
|
||||
|
||||
PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2
|
||||
--------------------------------------------
|
||||
|
||||
1. This LICENSE AGREEMENT is between the Python Software Foundation
|
||||
("PSF"), and the Individual or Organization ("Licensee") accessing and
|
||||
otherwise using this software ("Python") in source or binary form and
|
||||
its associated documentation.
|
||||
|
||||
2. Subject to the terms and conditions of this License Agreement, PSF hereby
|
||||
grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce,
|
||||
analyze, test, perform and/or display publicly, prepare derivative works,
|
||||
distribute, and otherwise use Python alone or in any derivative version,
|
||||
provided, however, that PSF's License Agreement and PSF's notice of copyright,
|
||||
i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010,
|
||||
2011, 2012 Python Software Foundation; All Rights Reserved" are retained in Python
|
||||
alone or in any derivative version prepared by Licensee.
|
||||
|
||||
3. In the event Licensee prepares a derivative work that is based on
|
||||
or incorporates Python or any part thereof, and wants to make
|
||||
the derivative work available to others as provided herein, then
|
||||
Licensee hereby agrees to include in any such work a brief summary of
|
||||
the changes made to Python.
|
||||
|
||||
4. PSF is making Python available to Licensee on an "AS IS"
|
||||
basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
|
||||
IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND
|
||||
DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
|
||||
FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT
|
||||
INFRINGE ANY THIRD PARTY RIGHTS.
|
||||
|
||||
5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON
|
||||
FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS
|
||||
A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON,
|
||||
OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
|
||||
|
||||
6. This License Agreement will automatically terminate upon a material
|
||||
breach of its terms and conditions.
|
||||
|
||||
7. Nothing in this License Agreement shall be deemed to create any
|
||||
relationship of agency, partnership, or joint venture between PSF and
|
||||
Licensee. This License Agreement does not grant permission to use PSF
|
||||
trademarks or trade name in a trademark sense to endorse or promote
|
||||
products or services of Licensee, or any third party.
|
||||
|
||||
8. By copying, installing or otherwise using Python, Licensee
|
||||
agrees to be bound by the terms and conditions of this License
|
||||
Agreement.
|
||||
|
||||
|
||||
BEOPEN.COM LICENSE AGREEMENT FOR PYTHON 2.0
|
||||
-------------------------------------------
|
||||
|
||||
BEOPEN PYTHON OPEN SOURCE LICENSE AGREEMENT VERSION 1
|
||||
|
||||
1. This LICENSE AGREEMENT is between BeOpen.com ("BeOpen"), having an
|
||||
office at 160 Saratoga Avenue, Santa Clara, CA 95051, and the
|
||||
Individual or Organization ("Licensee") accessing and otherwise using
|
||||
this software in source or binary form and its associated
|
||||
documentation ("the Software").
|
||||
|
||||
2. Subject to the terms and conditions of this BeOpen Python License
|
||||
Agreement, BeOpen hereby grants Licensee a non-exclusive,
|
||||
royalty-free, world-wide license to reproduce, analyze, test, perform
|
||||
and/or display publicly, prepare derivative works, distribute, and
|
||||
otherwise use the Software alone or in any derivative version,
|
||||
provided, however, that the BeOpen Python License is retained in the
|
||||
Software, alone or in any derivative version prepared by Licensee.
|
||||
|
||||
3. BeOpen is making the Software available to Licensee on an "AS IS"
|
||||
basis. BEOPEN MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
|
||||
IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, BEOPEN MAKES NO AND
|
||||
DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
|
||||
FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF THE SOFTWARE WILL NOT
|
||||
INFRINGE ANY THIRD PARTY RIGHTS.
|
||||
|
||||
4. BEOPEN SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF THE
|
||||
SOFTWARE FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS
|
||||
AS A RESULT OF USING, MODIFYING OR DISTRIBUTING THE SOFTWARE, OR ANY
|
||||
DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
|
||||
|
||||
5. This License Agreement will automatically terminate upon a material
|
||||
breach of its terms and conditions.
|
||||
|
||||
6. This License Agreement shall be governed by and interpreted in all
|
||||
respects by the law of the State of California, excluding conflict of
|
||||
law provisions. Nothing in this License Agreement shall be deemed to
|
||||
create any relationship of agency, partnership, or joint venture
|
||||
between BeOpen and Licensee. This License Agreement does not grant
|
||||
permission to use BeOpen trademarks or trade names in a trademark
|
||||
sense to endorse or promote products or services of Licensee, or any
|
||||
third party. As an exception, the "BeOpen Python" logos available at
|
||||
http://www.pythonlabs.com/logos.html may be used according to the
|
||||
permissions granted on that web page.
|
||||
|
||||
7. By copying, installing or otherwise using the software, Licensee
|
||||
agrees to be bound by the terms and conditions of this License
|
||||
Agreement.
|
||||
|
||||
|
||||
CNRI LICENSE AGREEMENT FOR PYTHON 1.6.1
|
||||
---------------------------------------
|
||||
|
||||
1. This LICENSE AGREEMENT is between the Corporation for National
|
||||
Research Initiatives, having an office at 1895 Preston White Drive,
|
||||
Reston, VA 20191 ("CNRI"), and the Individual or Organization
|
||||
("Licensee") accessing and otherwise using Python 1.6.1 software in
|
||||
source or binary form and its associated documentation.
|
||||
|
||||
2. Subject to the terms and conditions of this License Agreement, CNRI
|
||||
hereby grants Licensee a nonexclusive, royalty-free, world-wide
|
||||
license to reproduce, analyze, test, perform and/or display publicly,
|
||||
prepare derivative works, distribute, and otherwise use Python 1.6.1
|
||||
alone or in any derivative version, provided, however, that CNRI's
|
||||
License Agreement and CNRI's notice of copyright, i.e., "Copyright (c)
|
||||
1995-2001 Corporation for National Research Initiatives; All Rights
|
||||
Reserved" are retained in Python 1.6.1 alone or in any derivative
|
||||
version prepared by Licensee. Alternately, in lieu of CNRI's License
|
||||
Agreement, Licensee may substitute the following text (omitting the
|
||||
quotes): "Python 1.6.1 is made available subject to the terms and
|
||||
conditions in CNRI's License Agreement. This Agreement together with
|
||||
Python 1.6.1 may be located on the Internet using the following
|
||||
unique, persistent identifier (known as a handle): 1895.22/1013. This
|
||||
Agreement may also be obtained from a proxy server on the Internet
|
||||
using the following URL: http://hdl.handle.net/1895.22/1013".
|
||||
|
||||
3. In the event Licensee prepares a derivative work that is based on
|
||||
or incorporates Python 1.6.1 or any part thereof, and wants to make
|
||||
the derivative work available to others as provided herein, then
|
||||
Licensee hereby agrees to include in any such work a brief summary of
|
||||
the changes made to Python 1.6.1.
|
||||
|
||||
4. CNRI is making Python 1.6.1 available to Licensee on an "AS IS"
|
||||
basis. CNRI MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
|
||||
IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, CNRI MAKES NO AND
|
||||
DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
|
||||
FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON 1.6.1 WILL NOT
|
||||
INFRINGE ANY THIRD PARTY RIGHTS.
|
||||
|
||||
5. CNRI SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON
|
||||
1.6.1 FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS
|
||||
A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON 1.6.1,
|
||||
OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
|
||||
|
||||
6. This License Agreement will automatically terminate upon a material
|
||||
breach of its terms and conditions.
|
||||
|
||||
7. This License Agreement shall be governed by the federal
|
||||
intellectual property law of the United States, including without
|
||||
limitation the federal copyright law, and, to the extent such
|
||||
U.S. federal law does not apply, by the law of the Commonwealth of
|
||||
Virginia, excluding Virginia's conflict of law provisions.
|
||||
Notwithstanding the foregoing, with regard to derivative works based
|
||||
on Python 1.6.1 that incorporate non-separable material that was
|
||||
previously distributed under the GNU General Public License (GPL), the
|
||||
law of the Commonwealth of Virginia shall govern this License
|
||||
Agreement only as to issues arising under or with respect to
|
||||
Paragraphs 4, 5, and 7 of this License Agreement. Nothing in this
|
||||
License Agreement shall be deemed to create any relationship of
|
||||
agency, partnership, or joint venture between CNRI and Licensee. This
|
||||
License Agreement does not grant permission to use CNRI trademarks or
|
||||
trade name in a trademark sense to endorse or promote products or
|
||||
services of Licensee, or any third party.
|
||||
|
||||
8. By clicking on the "ACCEPT" button where indicated, or by copying,
|
||||
installing or otherwise using Python 1.6.1, Licensee agrees to be
|
||||
bound by the terms and conditions of this License Agreement.
|
||||
|
||||
ACCEPT
|
||||
|
||||
|
||||
CWI LICENSE AGREEMENT FOR PYTHON 0.9.0 THROUGH 1.2
|
||||
--------------------------------------------------
|
||||
|
||||
Copyright (c) 1991 - 1995, Stichting Mathematisch Centrum Amsterdam,
|
||||
The Netherlands. All rights reserved.
|
||||
|
||||
Permission to use, copy, modify, and distribute this software and its
|
||||
documentation for any purpose and without fee is hereby granted,
|
||||
provided that the above copyright notice appear in all copies and that
|
||||
both that copyright notice and this permission notice appear in
|
||||
supporting documentation, and that the name of Stichting Mathematisch
|
||||
Centrum or CWI not be used in advertising or publicity pertaining to
|
||||
distribution of the software without specific, written prior
|
||||
permission.
|
||||
|
||||
STICHTING MATHEMATISCH CENTRUM DISCLAIMS ALL WARRANTIES WITH REGARD TO
|
||||
THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
|
||||
FITNESS, IN NO EVENT SHALL STICHTING MATHEMATISCH CENTRUM BE LIABLE
|
||||
FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
|
||||
OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
340
web/documentserver-example/python/licenses/pylint.license
Normal file
340
web/documentserver-example/python/licenses/pylint.license
Normal file
@ -0,0 +1,340 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 2, June 1991
|
||||
|
||||
Copyright (C) 1989, 1991 Free Software Foundation, Inc.
|
||||
51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The licenses for most software are designed to take away your
|
||||
freedom to share and change it. By contrast, the GNU General Public
|
||||
License is intended to guarantee your freedom to share and change free
|
||||
software--to make sure the software is free for all its users. This
|
||||
General Public License applies to most of the Free Software
|
||||
Foundation's software and to any other program whose authors commit to
|
||||
using it. (Some other Free Software Foundation software is covered by
|
||||
the GNU Library General Public License instead.) You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
this service if you wish), that you receive source code or can get it
|
||||
if you want it, that you can change the software or use pieces of it
|
||||
in new free programs; and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to make restrictions that forbid
|
||||
anyone to deny you these rights or to ask you to surrender the rights.
|
||||
These restrictions translate to certain responsibilities for you if you
|
||||
distribute copies of the software, or if you modify it.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must give the recipients all the rights that
|
||||
you have. You must make sure that they, too, receive or can get the
|
||||
source code. And you must show them these terms so they know their
|
||||
rights.
|
||||
|
||||
We protect your rights with two steps: (1) copyright the software, and
|
||||
(2) offer you this license which gives you legal permission to copy,
|
||||
distribute and/or modify the software.
|
||||
|
||||
Also, for each author's protection and ours, we want to make certain
|
||||
that everyone understands that there is no warranty for this free
|
||||
software. If the software is modified by someone else and passed on, we
|
||||
want its recipients to know that what they have is not the original, so
|
||||
that any problems introduced by others will not reflect on the original
|
||||
authors' reputations.
|
||||
|
||||
Finally, any free program is threatened constantly by software
|
||||
patents. We wish to avoid the danger that redistributors of a free
|
||||
program will individually obtain patent licenses, in effect making the
|
||||
program proprietary. To prevent this, we have made it clear that any
|
||||
patent must be licensed for everyone's free use or not licensed at all.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||
|
||||
0. This License applies to any program or other work which contains
|
||||
a notice placed by the copyright holder saying it may be distributed
|
||||
under the terms of this General Public License. The "Program", below,
|
||||
refers to any such program or work, and a "work based on the Program"
|
||||
means either the Program or any derivative work under copyright law:
|
||||
that is to say, a work containing the Program or a portion of it,
|
||||
either verbatim or with modifications and/or translated into another
|
||||
language. (Hereinafter, translation is included without limitation in
|
||||
the term "modification".) Each licensee is addressed as "you".
|
||||
|
||||
Activities other than copying, distribution and modification are not
|
||||
covered by this License; they are outside its scope. The act of
|
||||
running the Program is not restricted, and the output from the Program
|
||||
is covered only if its contents constitute a work based on the
|
||||
Program (independent of having been made by running the Program).
|
||||
Whether that is true depends on what the Program does.
|
||||
|
||||
1. You may copy and distribute verbatim copies of the Program's
|
||||
source code as you receive it, in any medium, provided that you
|
||||
conspicuously and appropriately publish on each copy an appropriate
|
||||
copyright notice and disclaimer of warranty; keep intact all the
|
||||
notices that refer to this License and to the absence of any warranty;
|
||||
and give any other recipients of the Program a copy of this License
|
||||
along with the Program.
|
||||
|
||||
You may charge a fee for the physical act of transferring a copy, and
|
||||
you may at your option offer warranty protection in exchange for a fee.
|
||||
|
||||
2. You may modify your copy or copies of the Program or any portion
|
||||
of it, thus forming a work based on the Program, and copy and
|
||||
distribute such modifications or work under the terms of Section 1
|
||||
above, provided that you also meet all of these conditions:
|
||||
|
||||
a) You must cause the modified files to carry prominent notices
|
||||
stating that you changed the files and the date of any change.
|
||||
|
||||
b) You must cause any work that you distribute or publish, that in
|
||||
whole or in part contains or is derived from the Program or any
|
||||
part thereof, to be licensed as a whole at no charge to all third
|
||||
parties under the terms of this License.
|
||||
|
||||
c) If the modified program normally reads commands interactively
|
||||
when run, you must cause it, when started running for such
|
||||
interactive use in the most ordinary way, to print or display an
|
||||
announcement including an appropriate copyright notice and a
|
||||
notice that there is no warranty (or else, saying that you provide
|
||||
a warranty) and that users may redistribute the program under
|
||||
these conditions, and telling the user how to view a copy of this
|
||||
License. (Exception: if the Program itself is interactive but
|
||||
does not normally print such an announcement, your work based on
|
||||
the Program is not required to print an announcement.)
|
||||
|
||||
These requirements apply to the modified work as a whole. If
|
||||
identifiable sections of that work are not derived from the Program,
|
||||
and can be reasonably considered independent and separate works in
|
||||
themselves, then this License, and its terms, do not apply to those
|
||||
sections when you distribute them as separate works. But when you
|
||||
distribute the same sections as part of a whole which is a work based
|
||||
on the Program, the distribution of the whole must be on the terms of
|
||||
this License, whose permissions for other licensees extend to the
|
||||
entire whole, and thus to each and every part regardless of who wrote it.
|
||||
|
||||
Thus, it is not the intent of this section to claim rights or contest
|
||||
your rights to work written entirely by you; rather, the intent is to
|
||||
exercise the right to control the distribution of derivative or
|
||||
collective works based on the Program.
|
||||
|
||||
In addition, mere aggregation of another work not based on the Program
|
||||
with the Program (or with a work based on the Program) on a volume of
|
||||
a storage or distribution medium does not bring the other work under
|
||||
the scope of this License.
|
||||
|
||||
3. You may copy and distribute the Program (or a work based on it,
|
||||
under Section 2) in object code or executable form under the terms of
|
||||
Sections 1 and 2 above provided that you also do one of the following:
|
||||
|
||||
a) Accompany it with the complete corresponding machine-readable
|
||||
source code, which must be distributed under the terms of Sections
|
||||
1 and 2 above on a medium customarily used for software interchange; or,
|
||||
|
||||
b) Accompany it with a written offer, valid for at least three
|
||||
years, to give any third party, for a charge no more than your
|
||||
cost of physically performing source distribution, a complete
|
||||
machine-readable copy of the corresponding source code, to be
|
||||
distributed under the terms of Sections 1 and 2 above on a medium
|
||||
customarily used for software interchange; or,
|
||||
|
||||
c) Accompany it with the information you received as to the offer
|
||||
to distribute corresponding source code. (This alternative is
|
||||
allowed only for noncommercial distribution and only if you
|
||||
received the program in object code or executable form with such
|
||||
an offer, in accord with Subsection b above.)
|
||||
|
||||
The source code for a work means the preferred form of the work for
|
||||
making modifications to it. For an executable work, complete source
|
||||
code means all the source code for all modules it contains, plus any
|
||||
associated interface definition files, plus the scripts used to
|
||||
control compilation and installation of the executable. However, as a
|
||||
special exception, the source code distributed need not include
|
||||
anything that is normally distributed (in either source or binary
|
||||
form) with the major components (compiler, kernel, and so on) of the
|
||||
operating system on which the executable runs, unless that component
|
||||
itself accompanies the executable.
|
||||
|
||||
If distribution of executable or object code is made by offering
|
||||
access to copy from a designated place, then offering equivalent
|
||||
access to copy the source code from the same place counts as
|
||||
distribution of the source code, even though third parties are not
|
||||
compelled to copy the source along with the object code.
|
||||
|
||||
4. You may not copy, modify, sublicense, or distribute the Program
|
||||
except as expressly provided under this License. Any attempt
|
||||
otherwise to copy, modify, sublicense or distribute the Program is
|
||||
void, and will automatically terminate your rights under this License.
|
||||
However, parties who have received copies, or rights, from you under
|
||||
this License will not have their licenses terminated so long as such
|
||||
parties remain in full compliance.
|
||||
|
||||
5. You are not required to accept this License, since you have not
|
||||
signed it. However, nothing else grants you permission to modify or
|
||||
distribute the Program or its derivative works. These actions are
|
||||
prohibited by law if you do not accept this License. Therefore, by
|
||||
modifying or distributing the Program (or any work based on the
|
||||
Program), you indicate your acceptance of this License to do so, and
|
||||
all its terms and conditions for copying, distributing or modifying
|
||||
the Program or works based on it.
|
||||
|
||||
6. Each time you redistribute the Program (or any work based on the
|
||||
Program), the recipient automatically receives a license from the
|
||||
original licensor to copy, distribute or modify the Program subject to
|
||||
these terms and conditions. You may not impose any further
|
||||
restrictions on the recipients' exercise of the rights granted herein.
|
||||
You are not responsible for enforcing compliance by third parties to
|
||||
this License.
|
||||
|
||||
7. If, as a consequence of a court judgment or allegation of patent
|
||||
infringement or for any other reason (not limited to patent issues),
|
||||
conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot
|
||||
distribute so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you
|
||||
may not distribute the Program at all. For example, if a patent
|
||||
license would not permit royalty-free redistribution of the Program by
|
||||
all those who receive copies directly or indirectly through you, then
|
||||
the only way you could satisfy both it and this License would be to
|
||||
refrain entirely from distribution of the Program.
|
||||
|
||||
If any portion of this section is held invalid or unenforceable under
|
||||
any particular circumstance, the balance of the section is intended to
|
||||
apply and the section as a whole is intended to apply in other
|
||||
circumstances.
|
||||
|
||||
It is not the purpose of this section to induce you to infringe any
|
||||
patents or other property right claims or to contest validity of any
|
||||
such claims; this section has the sole purpose of protecting the
|
||||
integrity of the free software distribution system, which is
|
||||
implemented by public license practices. Many people have made
|
||||
generous contributions to the wide range of software distributed
|
||||
through that system in reliance on consistent application of that
|
||||
system; it is up to the author/donor to decide if he or she is willing
|
||||
to distribute software through any other system and a licensee cannot
|
||||
impose that choice.
|
||||
|
||||
This section is intended to make thoroughly clear what is believed to
|
||||
be a consequence of the rest of this License.
|
||||
|
||||
8. If the distribution and/or use of the Program is restricted in
|
||||
certain countries either by patents or by copyrighted interfaces, the
|
||||
original copyright holder who places the Program under this License
|
||||
may add an explicit geographical distribution limitation excluding
|
||||
those countries, so that distribution is permitted only in or among
|
||||
countries not thus excluded. In such case, this License incorporates
|
||||
the limitation as if written in the body of this License.
|
||||
|
||||
9. The Free Software Foundation may publish revised and/or new versions
|
||||
of the General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the Program
|
||||
specifies a version number of this License which applies to it and "any
|
||||
later version", you have the option of following the terms and conditions
|
||||
either of that version or of any later version published by the Free
|
||||
Software Foundation. If the Program does not specify a version number of
|
||||
this License, you may choose any version ever published by the Free Software
|
||||
Foundation.
|
||||
|
||||
10. If you wish to incorporate parts of the Program into other free
|
||||
programs whose distribution conditions are different, write to the author
|
||||
to ask for permission. For software which is copyrighted by the Free
|
||||
Software Foundation, write to the Free Software Foundation; we sometimes
|
||||
make exceptions for this. Our decision will be guided by the two goals
|
||||
of preserving the free status of all derivatives of our free software and
|
||||
of promoting the sharing and reuse of software generally.
|
||||
|
||||
NO WARRANTY
|
||||
|
||||
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
|
||||
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
|
||||
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
|
||||
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
|
||||
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
|
||||
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
|
||||
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
|
||||
REPAIR OR CORRECTION.
|
||||
|
||||
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
|
||||
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
|
||||
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
|
||||
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
|
||||
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
|
||||
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGES.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
convey the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation; either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program; if not, write to the Free Software
|
||||
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program is interactive, make it output a short notice like this
|
||||
when it starts in an interactive mode:
|
||||
|
||||
Gnomovision version 69, Copyright (C) year name of author
|
||||
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, the commands you use may
|
||||
be called something other than `show w' and `show c'; they could even be
|
||||
mouse-clicks or menu items--whatever suits your program.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or your
|
||||
school, if any, to sign a "copyright disclaimer" for the program, if
|
||||
necessary. Here is a sample; alter the names:
|
||||
|
||||
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
|
||||
`Gnomovision' (which makes passes at compilers) written by James Hacker.
|
||||
|
||||
<signature of Ty Coon>, 1 April 1989
|
||||
Ty Coon, President of Vice
|
||||
|
||||
This General Public License does not permit incorporating your program into
|
||||
proprietary programs. If your program is a subroutine library, you may
|
||||
consider it more useful to permit linking proprietary applications with the
|
||||
library. If this is what you want to do, use the GNU Library General
|
||||
Public License instead of this License.
|
||||
@ -1,21 +1,94 @@
|
||||
#!/usr/bin/env python
|
||||
"""Django's command-line utility for administrative tasks."""
|
||||
import os
|
||||
import sys
|
||||
from os import environ
|
||||
from sys import argv
|
||||
from uuid import uuid1
|
||||
from mimetypes import add_type
|
||||
from django import setup
|
||||
from django.conf import settings
|
||||
from django.core.management import execute_from_command_line
|
||||
from django.core.management.commands.runserver import Command as RunServer
|
||||
from django.urls import path
|
||||
from src.history import HistoryController
|
||||
from src.views import actions, index
|
||||
|
||||
def debug():
|
||||
env = environ.get('DEBUG')
|
||||
if env is None:
|
||||
return False
|
||||
if env == 'true':
|
||||
return True
|
||||
return False
|
||||
|
||||
def main():
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'src.settings')
|
||||
try:
|
||||
from django.core.management import execute_from_command_line
|
||||
except ImportError as exc:
|
||||
raise ImportError(
|
||||
"Couldn't import Django. Are you sure it's installed and "
|
||||
"available on your PYTHONPATH environment variable? Did you "
|
||||
"forget to activate a virtual environment?"
|
||||
) from exc
|
||||
execute_from_command_line(sys.argv)
|
||||
def address():
|
||||
if settings.DEBUG:
|
||||
return '127.0.0.1'
|
||||
return '0.0.0.0'
|
||||
|
||||
def port():
|
||||
env = environ.get('PORT')
|
||||
return env or '8000'
|
||||
|
||||
def configuration():
|
||||
return {
|
||||
'ALLOWED_HOSTS': [
|
||||
'*'
|
||||
],
|
||||
'DEBUG': debug(),
|
||||
'ROOT_URLCONF': __name__,
|
||||
'SECRET_KEY': uuid1(),
|
||||
'STATIC_URL': 'static/',
|
||||
'TEMPLATES': [
|
||||
{
|
||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||
'DIRS': [
|
||||
'templates'
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
def routers():
|
||||
history = HistoryController()
|
||||
return [
|
||||
path('', index.default),
|
||||
path('convert', actions.convert),
|
||||
path('create', actions.createNew),
|
||||
path('csv', actions.csv),
|
||||
path('download', actions.download),
|
||||
path('downloadhistory', actions.downloadhistory),
|
||||
path('edit', actions.edit),
|
||||
path('files', actions.files),
|
||||
path('reference', actions.reference),
|
||||
path('remove', actions.remove),
|
||||
path('rename', actions.rename),
|
||||
path('saveas', actions.saveAs),
|
||||
path('track', actions.track),
|
||||
path('upload', actions.upload),
|
||||
|
||||
path(
|
||||
'history/<str:source_basename>',
|
||||
history.history
|
||||
),
|
||||
path(
|
||||
'history/<str:source_basename>/<int:version>/data',
|
||||
history.data
|
||||
),
|
||||
path(
|
||||
'history/<str:source_basename>/<int:version>/download/<str:basename>',
|
||||
history.download
|
||||
),
|
||||
path(
|
||||
'history/<str:source_basename>/<int:version>/restore',
|
||||
history.restore
|
||||
)
|
||||
]
|
||||
|
||||
add_type('text/javascript', '.js', True)
|
||||
settings.configure(**configuration())
|
||||
urlpatterns = routers()
|
||||
RunServer.default_addr = address()
|
||||
# False positive: the default_port isn't an int, it's a str.
|
||||
RunServer.default_port = port() # type: ignore # noqa: E261
|
||||
setup()
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
execute_from_command_line(argv)
|
||||
|
||||
43
web/documentserver-example/python/proxy/nginx.conf
Normal file
43
web/documentserver-example/python/proxy/nginx.conf
Normal file
@ -0,0 +1,43 @@
|
||||
worker_processes auto;
|
||||
|
||||
events {
|
||||
worker_connections 512;
|
||||
}
|
||||
|
||||
http {
|
||||
include /etc/nginx/mime.types;
|
||||
|
||||
server {
|
||||
listen 8080;
|
||||
server_name localhost;
|
||||
|
||||
location / {
|
||||
proxy_http_version 1.1;
|
||||
proxy_pass http://example;
|
||||
}
|
||||
|
||||
location /static {
|
||||
alias /srv/static;
|
||||
autoindex on;
|
||||
}
|
||||
}
|
||||
|
||||
server {
|
||||
listen 3000;
|
||||
server_name localhost;
|
||||
|
||||
location / {
|
||||
client_max_body_size 100m;
|
||||
proxy_http_version 1.1;
|
||||
proxy_pass http://document-server;
|
||||
proxy_redirect off;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Host $http_x_forwarded_host;
|
||||
proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
}
|
||||
}
|
||||
}
|
||||
46
web/documentserver-example/python/pyproject.toml
Normal file
46
web/documentserver-example/python/pyproject.toml
Normal file
@ -0,0 +1,46 @@
|
||||
[build-system]
|
||||
requires = [
|
||||
"setuptools>=67.6.0"
|
||||
]
|
||||
|
||||
[project]
|
||||
name = "online-editor-example"
|
||||
version = "4.1.0"
|
||||
requires-python = ">=3.11"
|
||||
dependencies = [
|
||||
"django>=3.1.3",
|
||||
"django-stubs>=4.2.3",
|
||||
"pyjwt>=2.6.0",
|
||||
"python-magic>=0.4.27",
|
||||
"requests>=2.25.0"
|
||||
]
|
||||
|
||||
[project.license]
|
||||
text = "Apache-2.0"
|
||||
|
||||
[[project.authors]]
|
||||
name = "ONLYOFFICE"
|
||||
email = "support@onlyoffice.com"
|
||||
|
||||
[project.optional-dependencies]
|
||||
development = [
|
||||
"mypy>=1.4.1",
|
||||
"pylint>=2.17.4",
|
||||
"types-requests>=2.31.0"
|
||||
]
|
||||
|
||||
[tool.mypy]
|
||||
plugins = [
|
||||
"mypy_django_plugin.main"
|
||||
]
|
||||
|
||||
[tool.pylint]
|
||||
disable = [
|
||||
"missing-module-docstring",
|
||||
"missing-class-docstring",
|
||||
"missing-function-docstring"
|
||||
]
|
||||
class-const-naming-style = "snake_case"
|
||||
|
||||
[tool.django-stubs]
|
||||
django_settings_module = "manage"
|
||||
60
web/documentserver-example/python/src/codable/__init__.py
Normal file
60
web/documentserver-example/python/src/codable/__init__.py
Normal file
@ -0,0 +1,60 @@
|
||||
#
|
||||
# (c) Copyright Ascensio System SIA 2023
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
'''
|
||||
The Codable module provides the ability to decode a string JSON into a class
|
||||
instance and encode it back. It also provides the ability to remap JSON keys and
|
||||
work with nested Codable instances.
|
||||
|
||||
```python
|
||||
from dataclasses import dataclass
|
||||
from src.codable import Codable, CodingKey
|
||||
|
||||
@dataclass
|
||||
class Parent(Codable):
|
||||
class CodingKeys(CodingKey):
|
||||
native_for_python: 'foreignForPython'
|
||||
|
||||
native_for_python: str
|
||||
```
|
||||
|
||||
The algorithm for converting JSON objects to Codable instances is far from
|
||||
efficient, but it's simple and compatible with new Python features.
|
||||
|
||||
Perhaps in the future, it would be worth replacing the local implementation with
|
||||
an external dependency that offers the same functionality, such as the
|
||||
relatively popular [dataclasses-json](https://github.com/lidatong/dataclasses-json).
|
||||
Unfortunately, this library is currently not friendly with type annotations (see [issues](https://github.com/lidatong/dataclasses-json/issues?q=is%3Aissue+annotations))
|
||||
and struggles with type inference (see [#227](https://github.com/lidatong/dataclasses-json/issues/227)).
|
||||
|
||||
On the other hand, developing the current implementation into a full-fledged
|
||||
library may be more attractive to us.
|
||||
'''
|
||||
|
||||
from .codable import Codable, CodingKey
|
||||
|
||||
# TODO: isolate Decoder and Encoder initialization.
|
||||
# Give the user the ability to override the decode and encode methods in order
|
||||
# to change the object_hook for a specific property. For instance, this can be
|
||||
# used to override the default behavior for ParseResult (urlparse).
|
||||
|
||||
# TODO: make the CodingKey definition optional.
|
||||
# If the class doesn't provide the CodingKey, we must also use the native
|
||||
# property names as foreign.
|
||||
|
||||
# TODO: add common presets.
|
||||
# When overriding a specific CodingKeys method, define a common preset for all
|
||||
# foreign keys. For example, convert all of them from camel case to snake case.
|
||||
189
web/documentserver-example/python/src/codable/codable.py
Normal file
189
web/documentserver-example/python/src/codable/codable.py
Normal file
@ -0,0 +1,189 @@
|
||||
#
|
||||
# (c) Copyright Ascensio System SIA 2023
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
from __future__ import annotations
|
||||
from copy import deepcopy
|
||||
from enum import StrEnum
|
||||
from json import JSONDecoder, JSONEncoder
|
||||
from typing import Any, Optional, Self, Type, get_args, get_origin, get_type_hints
|
||||
|
||||
class Monkey():
|
||||
key: str
|
||||
|
||||
def __init__(self, key: str = '_slugs'):
|
||||
self.key = key
|
||||
|
||||
def patch(self, obj: dict[str, Any]) -> dict[str, Any]:
|
||||
def inner(slug: list[str], value: Any):
|
||||
if isinstance(value, dict):
|
||||
value[self.key] = slug
|
||||
|
||||
for child_slug, child_value in value.items():
|
||||
inner(slug + [child_slug], child_value)
|
||||
|
||||
return
|
||||
|
||||
if isinstance(value, list):
|
||||
for child_value in value:
|
||||
inner(slug, child_value)
|
||||
|
||||
copied = deepcopy(obj)
|
||||
inner([], copied)
|
||||
return copied
|
||||
|
||||
def slugs(self, obj: dict[str, Any]) -> list[str]:
|
||||
return obj[self.key]
|
||||
|
||||
def clean(self, obj: dict[str, Any]) -> dict[str, Any]:
|
||||
copied = deepcopy(obj)
|
||||
del copied[self.key]
|
||||
return copied
|
||||
|
||||
class CodingKey(StrEnum):
|
||||
@classmethod
|
||||
def keywords(cls, obj: dict[str, Any]) -> dict[str, Any]:
|
||||
words = {}
|
||||
|
||||
for pair in list(cls):
|
||||
# Errors are false positives.
|
||||
native = pair.name # type: ignore
|
||||
foreign = pair.value # type: ignore
|
||||
value = obj.get(foreign)
|
||||
words[native] = value
|
||||
|
||||
return words
|
||||
|
||||
class Codable():
|
||||
__decoder = JSONDecoder()
|
||||
__encoder = JSONEncoder()
|
||||
__monkey = Monkey()
|
||||
|
||||
class CodingKeys(CodingKey):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def decode(cls, content: str) -> Self:
|
||||
decoded = cls.__decoder.decode(content)
|
||||
patched = cls.__monkey.patch(decoded)
|
||||
encoded = cls.__encoder.encode(patched)
|
||||
decoder = Decoder(
|
||||
monkey=cls.__monkey,
|
||||
cls=cls
|
||||
)
|
||||
return decoder.decode(encoded)
|
||||
|
||||
def encode(self) -> str:
|
||||
cls = type(self)
|
||||
encoder = Encoder(
|
||||
decoder=self.__decoder,
|
||||
cls=cls
|
||||
)
|
||||
return encoder.encode(self)
|
||||
|
||||
class Decoder(JSONDecoder):
|
||||
monkey: Monkey
|
||||
cls: Type[Codable]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
monkey: Monkey,
|
||||
cls: Type[Codable],
|
||||
**kwargs
|
||||
):
|
||||
self.monkey = monkey
|
||||
self.cls = cls
|
||||
kwargs['object_hook'] = self.__object_hook
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def __object_hook(self, obj):
|
||||
cls = self.cls
|
||||
|
||||
for foreign in self.monkey.slugs(obj):
|
||||
native = cls.CodingKeys(foreign).name
|
||||
|
||||
if native is None:
|
||||
return self.monkey.clean(obj)
|
||||
|
||||
types = get_type_hints(cls)
|
||||
cls = self.__find_codable(types[native])
|
||||
|
||||
if cls is None:
|
||||
return self.monkey.clean(obj)
|
||||
|
||||
cleaned = self.monkey.clean(obj)
|
||||
return self.__init_codable(cls, cleaned)
|
||||
|
||||
def __find_codable(self, cls: Type) -> Optional[Type[Codable]]:
|
||||
if issubclass(cls, Codable):
|
||||
return cls
|
||||
|
||||
if get_origin(cls) is list:
|
||||
item = get_args(cls)[0]
|
||||
return self.__find_codable(item)
|
||||
|
||||
return None
|
||||
|
||||
def __init_codable(self, cls: Type[Codable], obj: dict[str, Any]) -> Codable:
|
||||
keywords = cls.CodingKeys.keywords(obj)
|
||||
return cls(**keywords)
|
||||
|
||||
class Encoder(JSONEncoder):
|
||||
decoder: JSONDecoder
|
||||
cls: Type[Codable]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
decoder: JSONDecoder,
|
||||
cls: Type[Codable],
|
||||
indent: int = 2,
|
||||
**kwargs
|
||||
):
|
||||
self.decoder = decoder
|
||||
self.cls = cls
|
||||
kwargs['indent'] = indent
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def default(self, o):
|
||||
obj = {}
|
||||
|
||||
for pair in list(self.cls.CodingKeys):
|
||||
native = pair.name
|
||||
foreign = pair.value
|
||||
|
||||
if not hasattr(o, native):
|
||||
continue
|
||||
|
||||
value = getattr(o, native)
|
||||
obj[foreign] = self.__prepare_value(value)
|
||||
|
||||
return obj
|
||||
|
||||
def __prepare_value(self, value: Any) -> Any:
|
||||
if isinstance(value, Codable):
|
||||
return self.__prepare_codable(value)
|
||||
|
||||
if isinstance(value, list):
|
||||
return self.__prepare_list(value)
|
||||
|
||||
return value
|
||||
|
||||
def __prepare_codable(self, value: Codable) -> Any:
|
||||
content = value.encode()
|
||||
return self.decoder.decode(content)
|
||||
|
||||
def __prepare_list(self, value: list[Any]) -> list[Any]:
|
||||
mapped = map(self.__prepare_value, value)
|
||||
return list(mapped)
|
||||
154
web/documentserver-example/python/src/codable/codable_tests.py
Normal file
154
web/documentserver-example/python/src/codable/codable_tests.py
Normal file
@ -0,0 +1,154 @@
|
||||
#
|
||||
# (c) Copyright Ascensio System SIA 2023
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
from __future__ import annotations
|
||||
from dataclasses import dataclass
|
||||
from textwrap import dedent
|
||||
from typing import Optional
|
||||
from unittest import TestCase
|
||||
from . import Codable, CodingKey
|
||||
|
||||
@dataclass
|
||||
class Fruit(Codable):
|
||||
class CodingKeys(CodingKey):
|
||||
name = 'fruit_name'
|
||||
weight = 'fruitWeight'
|
||||
texture = 'fruit_texture'
|
||||
vitamins = 'fruitVitamins'
|
||||
organic = 'fruit_organic'
|
||||
|
||||
name: str
|
||||
weight: int
|
||||
texture: Optional[str]
|
||||
vitamins: list[str]
|
||||
organic: bool
|
||||
|
||||
class CodablePlainTests(TestCase):
|
||||
json = (
|
||||
dedent(
|
||||
'''
|
||||
{
|
||||
"fruit_name": "kiwi",
|
||||
"fruitWeight": 100,
|
||||
"fruit_texture": null,
|
||||
"fruitVitamins": [
|
||||
"Vitamin C",
|
||||
"Vitamin K"
|
||||
],
|
||||
"fruit_organic": true
|
||||
}
|
||||
'''
|
||||
)
|
||||
.strip()
|
||||
)
|
||||
|
||||
def test_decodes(self):
|
||||
fruit = Fruit.decode(self.json)
|
||||
self.assertEqual(fruit.name, 'kiwi')
|
||||
self.assertEqual(fruit.weight, 100)
|
||||
self.assertIsNone(fruit.texture)
|
||||
self.assertEqual(fruit.vitamins, ['Vitamin C', 'Vitamin K'])
|
||||
self.assertTrue(fruit.organic)
|
||||
|
||||
def test_encodes(self):
|
||||
fruit = Fruit(
|
||||
name='kiwi',
|
||||
weight=100,
|
||||
texture=None,
|
||||
vitamins=['Vitamin C', 'Vitamin K'],
|
||||
organic=True
|
||||
)
|
||||
content = fruit.encode()
|
||||
self.assertEqual(content, self.json)
|
||||
|
||||
@dataclass
|
||||
class Smoothie(Codable):
|
||||
class CodingKeys(CodingKey):
|
||||
recipe = 'recipe'
|
||||
|
||||
recipe: Recipe
|
||||
|
||||
@dataclass
|
||||
class Recipe(Codable):
|
||||
class CodingKeys(CodingKey):
|
||||
ingredients = 'ingredients'
|
||||
|
||||
ingredients: list[Ingredient]
|
||||
|
||||
@dataclass
|
||||
class Ingredient(Codable):
|
||||
class CodingKeys(CodingKey):
|
||||
name = 'name'
|
||||
|
||||
name: str
|
||||
|
||||
class CodableNestedTests(TestCase):
|
||||
json = (
|
||||
dedent(
|
||||
'''
|
||||
{
|
||||
"recipe": {
|
||||
"ingredients": [
|
||||
{
|
||||
"name": "kiwi"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
'''
|
||||
)
|
||||
.strip()
|
||||
)
|
||||
|
||||
def test_decodes(self):
|
||||
smoothie = Smoothie.decode(self.json)
|
||||
self.assertEqual(smoothie.recipe.ingredients[0].name, 'kiwi')
|
||||
|
||||
def test_encodes(self):
|
||||
ingredient = Ingredient(name='kiwi')
|
||||
recipe = Recipe(ingredients=[ingredient])
|
||||
smoothie = Smoothie(recipe=recipe)
|
||||
content = smoothie.encode()
|
||||
self.assertEqual(content, self.json)
|
||||
|
||||
@dataclass
|
||||
class Vegetable(Codable):
|
||||
class CodingKeys(CodingKey):
|
||||
name = 'name'
|
||||
|
||||
name: Optional[str]
|
||||
|
||||
class CodableMissedTests(TestCase):
|
||||
source_json = '{}'
|
||||
distribute_json = (
|
||||
dedent(
|
||||
'''
|
||||
{
|
||||
"name": null
|
||||
}
|
||||
'''
|
||||
)
|
||||
.strip()
|
||||
)
|
||||
|
||||
def test_decodes(self):
|
||||
vegetable = Vegetable.decode(self.source_json)
|
||||
self.assertIsNone(vegetable.name)
|
||||
|
||||
def test_encodes(self):
|
||||
vegetable = Vegetable(name=None)
|
||||
content = vegetable.encode()
|
||||
self.assertEqual(content, self.distribute_json)
|
||||
18
web/documentserver-example/python/src/common/__init__.py
Normal file
18
web/documentserver-example/python/src/common/__init__.py
Normal file
@ -0,0 +1,18 @@
|
||||
#
|
||||
# (c) Copyright Ascensio System SIA 2023
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
from . import http
|
||||
from . import optional
|
||||
44
web/documentserver-example/python/src/common/http.py
Normal file
44
web/documentserver-example/python/src/common/http.py
Normal file
@ -0,0 +1,44 @@
|
||||
#
|
||||
# (c) Copyright Ascensio System SIA 2023
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
# TODO:
|
||||
# https://github.com/python/typing/discussions/946
|
||||
|
||||
from http import HTTPStatus, HTTPMethod
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
|
||||
# TODO: Access-Control-Allow-Origin
|
||||
def access_control_allow_origin():
|
||||
pass
|
||||
|
||||
def method(meth: HTTPMethod):
|
||||
def wrapper(func):
|
||||
def inner(self, request: HttpRequest, *args, **kwargs):
|
||||
if request.method is None:
|
||||
return HttpResponse(
|
||||
status=HTTPStatus.METHOD_NOT_ALLOWED
|
||||
)
|
||||
|
||||
if request.method.upper() != meth.name:
|
||||
return HttpResponse(
|
||||
status=HTTPStatus.METHOD_NOT_ALLOWED
|
||||
)
|
||||
|
||||
return func(self, request, *args, **kwargs)
|
||||
|
||||
return inner
|
||||
|
||||
return wrapper
|
||||
25
web/documentserver-example/python/src/common/optional.py
Normal file
25
web/documentserver-example/python/src/common/optional.py
Normal file
@ -0,0 +1,25 @@
|
||||
#
|
||||
# (c) Copyright Ascensio System SIA 2023
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
from typing import Callable, Optional, TypeVar
|
||||
|
||||
T = TypeVar('T')
|
||||
|
||||
def expression(callback: Callable[[], T]) -> Optional[T]:
|
||||
try:
|
||||
return callback()
|
||||
except Exception:
|
||||
return None
|
||||
@ -0,0 +1,19 @@
|
||||
"""
|
||||
|
||||
(c) Copyright Ascensio System SIA 2023
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
"""
|
||||
|
||||
from .configuration import *
|
||||
@ -0,0 +1,242 @@
|
||||
"""
|
||||
|
||||
(c) Copyright Ascensio System SIA 2023
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
"""
|
||||
|
||||
# pylint: disable=missing-module-docstring
|
||||
# pylint: disable=missing-class-docstring
|
||||
# pylint: disable=missing-function-docstring
|
||||
from os import environ
|
||||
from os.path import abspath, dirname
|
||||
from pathlib import Path
|
||||
from typing import Dict
|
||||
from urllib.parse import ParseResult, urlparse, urljoin
|
||||
|
||||
class ConfigurationManager:
|
||||
version = '1.5.1'
|
||||
|
||||
def example_url(self) -> (ParseResult | None):
|
||||
url = environ.get('EXAMPLE_URL')
|
||||
if not url:
|
||||
return None
|
||||
return urlparse(url)
|
||||
|
||||
def document_server_public_url(self) -> ParseResult:
|
||||
url = (
|
||||
environ.get('DOCUMENT_SERVER_PUBLIC_URL') or
|
||||
'http://document-server/'
|
||||
)
|
||||
return urlparse(url)
|
||||
|
||||
def document_server_private_url(self) -> ParseResult:
|
||||
url = environ.get('DOCUMENT_SERVER_PRIVATE_URL')
|
||||
if not url:
|
||||
return self.document_server_public_url()
|
||||
return urlparse(url)
|
||||
|
||||
def document_server_api_url(self) -> ParseResult:
|
||||
base = self.document_server_public_url().geturl()
|
||||
path = (
|
||||
environ.get('DOCUMENT_SERVER_API_PATH') or
|
||||
'web-apps/apps/api/documents/api.js'
|
||||
)
|
||||
url = urljoin(base, path)
|
||||
return urlparse(url)
|
||||
|
||||
def document_server_preloader_url(self) -> ParseResult:
|
||||
base = self.document_server_public_url().geturl()
|
||||
path = (
|
||||
environ.get('DOCUMENT_SERVER_PRELOADER_PATH') or
|
||||
'web-apps/apps/api/documents/cache-scripts.html'
|
||||
)
|
||||
url = urljoin(base, path)
|
||||
return urlparse(url)
|
||||
|
||||
def document_server_command_url(self) -> ParseResult:
|
||||
base = self.document_server_private_url().geturl()
|
||||
path = (
|
||||
environ.get('DOCUMENT_SERVER_COMMAND_PATH') or
|
||||
'coauthoring/CommandService.ashx'
|
||||
)
|
||||
url = urljoin(base, path)
|
||||
return urlparse(url)
|
||||
|
||||
def document_server_converter_url(self) -> ParseResult:
|
||||
base = self.document_server_private_url().geturl()
|
||||
path = (
|
||||
environ.get('DOCUMENT_SERVER_CONVERTER_PATH') or
|
||||
'ConvertService.ashx'
|
||||
)
|
||||
url = urljoin(base, path)
|
||||
return urlparse(url)
|
||||
|
||||
def jwt_secret(self) -> str:
|
||||
return environ.get('JWT_SECRET') or ''
|
||||
|
||||
def jwt_header(self) -> str:
|
||||
return environ.get('JWT_HEADER') or 'Authorization'
|
||||
|
||||
def jwt_use_for_request(self) -> bool:
|
||||
use = environ.get('JWT_USE_FOR_REQUEST')
|
||||
if use is None:
|
||||
return True
|
||||
if use == 'true':
|
||||
return True
|
||||
return False
|
||||
|
||||
def ssl_verify_peer_mode_enabled(self) -> bool:
|
||||
enabled = environ.get('SSL_VERIFY_PEER_MODE_ENABLED')
|
||||
if enabled is None:
|
||||
return False
|
||||
if enabled == 'true':
|
||||
return True
|
||||
return False
|
||||
|
||||
def storage_path(self) -> Path:
|
||||
storage_path = environ.get('STORAGE_PATH') or 'storage'
|
||||
storage_directory = Path(storage_path)
|
||||
if storage_directory.is_absolute():
|
||||
return storage_directory
|
||||
current_directory = Path(dirname(abspath(__file__)))
|
||||
return current_directory.joinpath('../..', storage_directory).resolve()
|
||||
|
||||
def maximum_file_size(self) -> int:
|
||||
size = environ.get('MAXIMUM_FILE_SIZE')
|
||||
if size:
|
||||
return int(size)
|
||||
return 5 * 1024 * 1024
|
||||
|
||||
def conversion_timeout(self) -> int:
|
||||
timeout = environ.get('CONVERSION_TIMEOUT')
|
||||
if timeout:
|
||||
return int(timeout)
|
||||
return 120 * 1000
|
||||
|
||||
def fillable_file_extensions(self) -> list[str]:
|
||||
return [
|
||||
'.docx',
|
||||
'.oform'
|
||||
]
|
||||
|
||||
def viewable_file_extensions(self) -> list[str]:
|
||||
return [
|
||||
'.djvu',
|
||||
'.oxps',
|
||||
'.pdf',
|
||||
'.xps'
|
||||
]
|
||||
|
||||
def editable_file_extensions(self) -> list[str]:
|
||||
return [
|
||||
'.csv', '.docm', '.docx',
|
||||
'.docxf', '.dotm', '.dotx',
|
||||
'.epub', '.fb2', '.html',
|
||||
'.odp', '.ods', '.odt',
|
||||
'.otp', '.ots', '.ott',
|
||||
'.potm', '.potx', '.ppsm',
|
||||
'.ppsx', '.pptm', '.pptx',
|
||||
'.rtf', '.txt', '.xlsm',
|
||||
'.xlsx', '.xltm', '.xltx'
|
||||
]
|
||||
|
||||
def convertible_file_extensions(self) -> list[str]:
|
||||
return [
|
||||
'.doc', '.dot', '.dps', '.dpt',
|
||||
'.epub', '.et', '.ett', '.fb2',
|
||||
'.fodp', '.fods', '.fodt', '.htm',
|
||||
'.html', '.mht', '.mhtml', '.odp',
|
||||
'.ods', '.odt', '.otp', '.ots',
|
||||
'.ott', '.pot', '.pps', '.ppt',
|
||||
'.rtf', '.stw', '.sxc', '.sxi',
|
||||
'.sxw', '.wps', '.wpt', '.xls',
|
||||
'.xlsb', '.xlt', '.xml'
|
||||
]
|
||||
|
||||
def spreadsheet_file_extensions(self) -> list[str]:
|
||||
return [
|
||||
'.xls', '.xlsx',
|
||||
'.xlsm', '.xlsb',
|
||||
'.xlt', '.xltx',
|
||||
'.xltm', '.ods',
|
||||
'.fods', '.ots',
|
||||
'.csv'
|
||||
]
|
||||
|
||||
def presentation_file_extensions(self) -> list[str]:
|
||||
return [
|
||||
'.pps', '.ppsx',
|
||||
'.ppsm', '.ppt',
|
||||
'.pptx', '.pptm',
|
||||
'.pot', '.potx',
|
||||
'.potm', '.odp',
|
||||
'.fodp', '.otp'
|
||||
]
|
||||
|
||||
def document_file_extensions(self) -> list[str]:
|
||||
return [
|
||||
'.doc', '.docx', '.docm',
|
||||
'.dot', '.dotx', '.dotm',
|
||||
'.odt', '.fodt', '.ott',
|
||||
'.rtf', '.txt', '.html',
|
||||
'.htm', '.mht', '.xml',
|
||||
'.pdf', '.djvu', '.fb2',
|
||||
'.epub', '.xps', '.oxps',
|
||||
'.oform'
|
||||
]
|
||||
|
||||
def languages(self) -> Dict[str, str]:
|
||||
return {
|
||||
'en': 'English',
|
||||
'hy': 'Armenian',
|
||||
'az': 'Azerbaijani',
|
||||
'eu': 'Basque',
|
||||
'be': 'Belarusian',
|
||||
'bg': 'Bulgarian',
|
||||
'ca': 'Catalan',
|
||||
'zh': 'Chinese (Simplified)',
|
||||
'zh-TW': 'Chinese (Traditional)',
|
||||
'cs': 'Czech',
|
||||
'da': 'Danish',
|
||||
'nl': 'Dutch',
|
||||
'fi': 'Finnish',
|
||||
'fr': 'French',
|
||||
'gl': 'Galego',
|
||||
'de': 'German',
|
||||
'el': 'Greek',
|
||||
'hu': 'Hungarian',
|
||||
'id': 'Indonesian',
|
||||
'it': 'Italian',
|
||||
'ja': 'Japanese',
|
||||
'ko': 'Korean',
|
||||
'lo': 'Lao',
|
||||
'lv': 'Latvian',
|
||||
'ms': 'Malay (Malaysia)',
|
||||
'no': 'Norwegian',
|
||||
'pl': 'Polish',
|
||||
'pt': 'Portuguese (Brazil)',
|
||||
'pt-PT': 'Portuguese (Portugal)',
|
||||
'ro': 'Romanian',
|
||||
'ru': 'Russian',
|
||||
'si': 'Sinhala (Sri Lanka)',
|
||||
'sk': 'Slovak',
|
||||
'sl': 'Slovenian',
|
||||
'es': 'Spanish',
|
||||
'sv': 'Swedish',
|
||||
'tr': 'Turkish',
|
||||
'uk': 'Ukrainian',
|
||||
'vi': 'Vietnamese',
|
||||
'aa-AA': 'Test Language'
|
||||
}
|
||||
@ -0,0 +1,301 @@
|
||||
"""
|
||||
|
||||
(c) Copyright Ascensio System SIA 2023
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
"""
|
||||
|
||||
# pylint: disable=missing-module-docstring
|
||||
# pylint: disable=missing-class-docstring
|
||||
# pylint: disable=missing-function-docstring
|
||||
from os import environ
|
||||
from unittest import TestCase
|
||||
from unittest.mock import patch
|
||||
from urllib.parse import urlparse
|
||||
from . import ConfigurationManager
|
||||
|
||||
class ConfigurationManagerTests(TestCase):
|
||||
def test_corresponds_the_latest_version(self):
|
||||
config = ConfigurationManager()
|
||||
self.assertEqual(config.version, '1.5.1')
|
||||
|
||||
class ConfigurationManagerExampleURLTests(TestCase):
|
||||
def test_assigns_a_default_value(self):
|
||||
config = ConfigurationManager()
|
||||
url = config.example_url()
|
||||
self.assertIsNone(url)
|
||||
|
||||
@patch.dict(environ, {
|
||||
'EXAMPLE_URL': 'http://localhost'
|
||||
})
|
||||
def test_assigns_a_value_from_the_environment(self):
|
||||
config = ConfigurationManager()
|
||||
url = config.example_url()
|
||||
self.assertEqual(url.geturl(), 'http://localhost')
|
||||
|
||||
class ConfigurationManagerDocumentServerPublicURLTests(TestCase):
|
||||
def test_assigns_a_default_value(self):
|
||||
config = ConfigurationManager()
|
||||
url = config.document_server_public_url()
|
||||
self.assertEqual(url.geturl(), 'http://document-server/')
|
||||
|
||||
@patch.dict(environ, {
|
||||
'DOCUMENT_SERVER_PUBLIC_URL': 'http://localhost'
|
||||
})
|
||||
def test_assigns_a_value_from_the_environment(self):
|
||||
config = ConfigurationManager()
|
||||
url = config.document_server_public_url()
|
||||
self.assertEqual(url.geturl(), 'http://localhost')
|
||||
|
||||
class ConfigurationManagerDocumentServerPrivateURLTests(TestCase):
|
||||
def test_assigns_a_default_value(self):
|
||||
config = ConfigurationManager()
|
||||
url = config.document_server_private_url()
|
||||
self.assertEqual(url.geturl(), 'http://document-server/')
|
||||
|
||||
@patch.dict(environ, {
|
||||
'DOCUMENT_SERVER_PRIVATE_URL': 'http://localhost'
|
||||
})
|
||||
def test_assigns_a_value_from_the_environment(self):
|
||||
config = ConfigurationManager()
|
||||
url = config.document_server_private_url()
|
||||
self.assertEqual(url.geturl(), 'http://localhost')
|
||||
|
||||
class ConfigurationManagerDocumentServerAPIURLTests(TestCase):
|
||||
@patch.object(
|
||||
ConfigurationManager,
|
||||
'document_server_public_url',
|
||||
return_value=urlparse('http://localhost')
|
||||
)
|
||||
def test_assigns_a_default_value(self, _):
|
||||
config = ConfigurationManager()
|
||||
url = config.document_server_api_url()
|
||||
self.assertEqual(
|
||||
url.geturl(),
|
||||
'http://localhost/web-apps/apps/api/documents/api.js'
|
||||
)
|
||||
|
||||
@patch.object(
|
||||
ConfigurationManager,
|
||||
'document_server_public_url',
|
||||
return_value=urlparse('http://localhost')
|
||||
)
|
||||
@patch.dict(environ, {
|
||||
'DOCUMENT_SERVER_API_PATH': '/api'
|
||||
})
|
||||
def test_assigns_a_value_from_the_environment(self, _):
|
||||
config = ConfigurationManager()
|
||||
url = config.document_server_api_url()
|
||||
self.assertEqual(
|
||||
url.geturl(),
|
||||
'http://localhost/api'
|
||||
)
|
||||
|
||||
class ConfigurationManagerDocumentServerPreloaderURLTests(TestCase):
|
||||
@patch.object(
|
||||
ConfigurationManager,
|
||||
'document_server_public_url',
|
||||
return_value=urlparse('http://localhost')
|
||||
)
|
||||
def test_assigns_a_default_value(self, _):
|
||||
config = ConfigurationManager()
|
||||
url = config.document_server_preloader_url()
|
||||
self.assertEqual(
|
||||
url.geturl(),
|
||||
'http://localhost/web-apps/apps/api/documents/cache-scripts.html'
|
||||
)
|
||||
|
||||
@patch.object(
|
||||
ConfigurationManager,
|
||||
'document_server_public_url',
|
||||
return_value=urlparse('http://localhost')
|
||||
)
|
||||
@patch.dict(environ, {
|
||||
'DOCUMENT_SERVER_PRELOADER_PATH': '/preloader'
|
||||
})
|
||||
def test_assigns_a_value_from_the_environment(self, _):
|
||||
config = ConfigurationManager()
|
||||
url = config.document_server_preloader_url()
|
||||
self.assertEqual(
|
||||
url.geturl(),
|
||||
'http://localhost/preloader'
|
||||
)
|
||||
|
||||
class ConfigurationManagerDocumentServerCommandURLTests(TestCase):
|
||||
@patch.object(
|
||||
ConfigurationManager,
|
||||
'document_server_private_url',
|
||||
return_value=urlparse('http://localhost')
|
||||
)
|
||||
def test_assigns_a_default_value(self, _):
|
||||
config = ConfigurationManager()
|
||||
url = config.document_server_command_url()
|
||||
self.assertEqual(
|
||||
url.geturl(),
|
||||
'http://localhost/coauthoring/CommandService.ashx'
|
||||
)
|
||||
|
||||
@patch.object(
|
||||
ConfigurationManager,
|
||||
'document_server_private_url',
|
||||
return_value=urlparse('http://localhost')
|
||||
)
|
||||
@patch.dict(environ, {
|
||||
'DOCUMENT_SERVER_COMMAND_PATH': '/command'
|
||||
})
|
||||
def test_assigns_a_value_from_the_environment(self, _):
|
||||
config = ConfigurationManager()
|
||||
url = config.document_server_command_url()
|
||||
self.assertEqual(
|
||||
url.geturl(),
|
||||
'http://localhost/command'
|
||||
)
|
||||
|
||||
class ConfigurationManagerDocumentServerConverterURLTests(TestCase):
|
||||
@patch.object(
|
||||
ConfigurationManager,
|
||||
'document_server_private_url',
|
||||
return_value=urlparse('http://localhost')
|
||||
)
|
||||
def test_assigns_a_default_value(self, _):
|
||||
config = ConfigurationManager()
|
||||
url = config.document_server_converter_url()
|
||||
self.assertEqual(
|
||||
url.geturl(),
|
||||
'http://localhost/ConvertService.ashx'
|
||||
)
|
||||
|
||||
@patch.object(
|
||||
ConfigurationManager,
|
||||
'document_server_private_url',
|
||||
return_value=urlparse('http://localhost')
|
||||
)
|
||||
@patch.dict(environ, {
|
||||
'DOCUMENT_SERVER_CONVERTER_PATH': '/converter'
|
||||
})
|
||||
def test_assigns_a_value_from_the_environment(self, _):
|
||||
config = ConfigurationManager()
|
||||
url = config.document_server_converter_url()
|
||||
self.assertEqual(
|
||||
url.geturl(),
|
||||
'http://localhost/converter'
|
||||
)
|
||||
|
||||
class ConfigurationManagerJWTSecretTests(TestCase):
|
||||
def test_assigns_a_default_value(self):
|
||||
config = ConfigurationManager()
|
||||
secret = config.jwt_secret()
|
||||
self.assertEqual(secret, '')
|
||||
|
||||
@patch.dict(environ, {
|
||||
'JWT_SECRET': 'your-256-bit-secret'
|
||||
})
|
||||
def test_assigns_a_value_from_the_environment(self):
|
||||
config = ConfigurationManager()
|
||||
secret = config.jwt_secret()
|
||||
self.assertEqual(secret, 'your-256-bit-secret')
|
||||
|
||||
class ConfigurationManagerJWTHeaderTests(TestCase):
|
||||
def test_assigns_a_default_value(self):
|
||||
config = ConfigurationManager()
|
||||
header = config.jwt_header()
|
||||
self.assertEqual(header, 'Authorization')
|
||||
|
||||
@patch.dict(environ, {
|
||||
'JWT_HEADER': 'Proxy-Authorization'
|
||||
})
|
||||
def test_assigns_a_value_from_the_environment(self):
|
||||
config = ConfigurationManager()
|
||||
header = config.jwt_header()
|
||||
self.assertEqual(header, 'Proxy-Authorization')
|
||||
|
||||
class ConfigurationManagerJWTUseForRequest(TestCase):
|
||||
def test_assigns_a_default_value(self):
|
||||
config = ConfigurationManager()
|
||||
use = config.jwt_use_for_request()
|
||||
self.assertTrue(use)
|
||||
|
||||
@patch.dict(environ, {
|
||||
'JWT_USE_FOR_REQUEST': 'false'
|
||||
})
|
||||
def test_assigns_a_value_from_the_environment(self):
|
||||
config = ConfigurationManager()
|
||||
use = config.jwt_use_for_request()
|
||||
self.assertFalse(use)
|
||||
|
||||
class ConfigurationManagerSSLTests(TestCase):
|
||||
def test_assigns_a_default_value(self):
|
||||
config = ConfigurationManager()
|
||||
enabled = config.ssl_verify_peer_mode_enabled()
|
||||
self.assertFalse(enabled)
|
||||
|
||||
@patch.dict(environ, {
|
||||
'SSL_VERIFY_PEER_MODE_ENABLED': 'true'
|
||||
})
|
||||
def test_assigns_a_value_from_the_environment(self):
|
||||
config = ConfigurationManager()
|
||||
enabled = config.ssl_verify_peer_mode_enabled()
|
||||
self.assertTrue(enabled)
|
||||
|
||||
class ConfigurationManagerStoragePathTests(TestCase):
|
||||
def test_assigns_a_default_value(self):
|
||||
config = ConfigurationManager()
|
||||
path = config.storage_path()
|
||||
self.assertTrue(path.is_absolute())
|
||||
self.assertEqual(path.name, 'storage')
|
||||
|
||||
@patch.dict(environ, {
|
||||
'STORAGE_PATH': 'directory'
|
||||
})
|
||||
def test_assigns_a_relative_path_from_the_environment(self):
|
||||
config = ConfigurationManager()
|
||||
path = config.storage_path()
|
||||
self.assertTrue(path.is_absolute())
|
||||
self.assertEqual(path.name, 'directory')
|
||||
|
||||
@patch.dict(environ, {
|
||||
'STORAGE_PATH': '/directory'
|
||||
})
|
||||
def test_assigns_an_absolute_path_from_the_environment(self):
|
||||
config = ConfigurationManager()
|
||||
path = config.storage_path()
|
||||
self.assertEqual(path.as_uri(), 'file:///directory')
|
||||
|
||||
class ConfigurationManagerMaximumFileSizeTests(TestCase):
|
||||
def test_assigns_a_default_value(self):
|
||||
config = ConfigurationManager()
|
||||
size = config.maximum_file_size()
|
||||
self.assertEqual(size, 5_242_880)
|
||||
|
||||
@patch.dict(environ, {
|
||||
'MAXIMUM_FILE_SIZE': '10'
|
||||
})
|
||||
def test_assigns_a_value_from_the_environment(self):
|
||||
config = ConfigurationManager()
|
||||
size = config.maximum_file_size()
|
||||
self.assertEqual(size, 10)
|
||||
|
||||
class ConfigurationManagerConversionTimeoutTests(TestCase):
|
||||
def test_assigns_a_default_value(self):
|
||||
config = ConfigurationManager()
|
||||
timeout = config.conversion_timeout()
|
||||
self.assertEqual(timeout, 120_000)
|
||||
|
||||
@patch.dict(environ, {
|
||||
'CONVERSION_TIMEOUT': '10'
|
||||
})
|
||||
def test_assigns_a_value_from_the_environment(self):
|
||||
config = ConfigurationManager()
|
||||
timeout = config.conversion_timeout()
|
||||
self.assertEqual(timeout, 10)
|
||||
19
web/documentserver-example/python/src/history/__init__.py
Normal file
19
web/documentserver-example/python/src/history/__init__.py
Normal file
@ -0,0 +1,19 @@
|
||||
#
|
||||
# (c) Copyright Ascensio System SIA 2023
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
# https://api.onlyoffice.com/editors/callback#history
|
||||
|
||||
from .history import *
|
||||
595
web/documentserver-example/python/src/history/history.py
Normal file
595
web/documentserver-example/python/src/history/history.py
Normal file
@ -0,0 +1,595 @@
|
||||
#
|
||||
# (c) Copyright Ascensio System SIA 2023
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
# TODO: add types for kwargs.
|
||||
# https://github.com/python/mypy/issues/14697
|
||||
|
||||
from __future__ import annotations
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from functools import reduce
|
||||
from http import HTTPMethod
|
||||
# from http import HTTPStatus
|
||||
from json import loads
|
||||
from pathlib import Path
|
||||
from shutil import copy
|
||||
from typing import Any, Iterator, Optional
|
||||
from uuid import uuid1
|
||||
from urllib.parse import \
|
||||
ParseResult, \
|
||||
parse_qs, \
|
||||
quote, \
|
||||
urlencode, \
|
||||
urljoin, \
|
||||
urlparse
|
||||
from django.http import FileResponse, HttpRequest, HttpResponse
|
||||
# Pylance doesn't see the HttpResponseBase export from the django.http.
|
||||
from django.http.response import HttpResponseBase
|
||||
from src.codable import Codable, CodingKey
|
||||
from src.configuration import ConfigurationManager
|
||||
from src.common import http, optional
|
||||
from src.request import RequestManager
|
||||
from src.storage import StorageManager
|
||||
from src.utils import jwtManager
|
||||
from src.utils.users import find_user
|
||||
|
||||
@dataclass
|
||||
class History(Codable):
|
||||
class CodingKeys(CodingKey):
|
||||
current_version = 'currentVersion'
|
||||
history = 'history'
|
||||
|
||||
current_version: int
|
||||
history: list[HistoryItem]
|
||||
|
||||
@dataclass
|
||||
class HistoryItem(Codable):
|
||||
class CodingKeys(CodingKey):
|
||||
changes = 'changes'
|
||||
created = 'created'
|
||||
key = 'key'
|
||||
server_version = 'serverVersion'
|
||||
user = 'user'
|
||||
version = 'version'
|
||||
|
||||
changes: list[HistoryChangesItem]
|
||||
created: str
|
||||
key: str
|
||||
server_version: Optional[str]
|
||||
user: Optional[HistoryUser]
|
||||
version: int
|
||||
|
||||
@dataclass
|
||||
class HistoryChanges(Codable):
|
||||
class CodingKeys(CodingKey):
|
||||
server_version = 'serverVersion'
|
||||
changes = 'changes'
|
||||
|
||||
server_version: Optional[str]
|
||||
changes: list[HistoryChangesItem]
|
||||
|
||||
@dataclass
|
||||
class HistoryChangesItem(Codable):
|
||||
class CodingKeys(CodingKey):
|
||||
created = 'created'
|
||||
user = 'user'
|
||||
|
||||
created: str
|
||||
user: HistoryUser
|
||||
|
||||
@dataclass
|
||||
class HistoryUser(Codable):
|
||||
class CodingKeys(CodingKey):
|
||||
id = 'id'
|
||||
name = 'name'
|
||||
|
||||
id: str
|
||||
name: str
|
||||
|
||||
@dataclass
|
||||
class HistoryData(Codable):
|
||||
class CodingKeys(CodingKey):
|
||||
changes_url = 'changesUrl'
|
||||
file_type = 'fileType'
|
||||
key = 'key'
|
||||
previous = 'previous'
|
||||
token = 'token'
|
||||
url = 'url'
|
||||
direct_url = 'directUrl'
|
||||
version = 'version'
|
||||
|
||||
changes_url: Optional[str]
|
||||
file_type: Optional[str]
|
||||
key: str
|
||||
previous: Optional[HistoryData]
|
||||
token: Optional[str]
|
||||
url: Optional[str]
|
||||
direct_url: Optional[str]
|
||||
version: int
|
||||
|
||||
class HistoryController():
|
||||
@http.method(HTTPMethod.GET)
|
||||
def history(self, request: HttpRequest, **kwargs: Any) -> HttpResponseBase:
|
||||
'''
|
||||
https://api.onlyoffice.com/editors/methods#refreshHistory
|
||||
|
||||
```http
|
||||
GET {{base_url}}/history/{{source_basename}}?userHost={{user_host}} HTTP/1.1
|
||||
```
|
||||
'''
|
||||
config_manager = ConfigurationManager()
|
||||
request_manager = RequestManager(
|
||||
request=request
|
||||
)
|
||||
|
||||
source_basename: str = kwargs['source_basename']
|
||||
optional_user_host = request.GET.get('userHost')
|
||||
user_host = request_manager.resolve_address(optional_user_host)
|
||||
|
||||
storage_manager = StorageManager(
|
||||
config_manager=config_manager,
|
||||
user_host=user_host,
|
||||
source_basename=source_basename
|
||||
)
|
||||
history_manager = HistoryManager(
|
||||
storage_manager=storage_manager
|
||||
)
|
||||
|
||||
history = history_manager.history()
|
||||
return HttpResponse(
|
||||
history.encode(),
|
||||
content_type='application/json'
|
||||
)
|
||||
|
||||
@http.method(HTTPMethod.GET)
|
||||
def data(self, request: HttpRequest, **kwargs: Any) -> HttpResponseBase:
|
||||
'''
|
||||
https://api.onlyoffice.com/editors/methods#setHistoryData
|
||||
|
||||
```http
|
||||
GET {{base_url}}/history/{{source_basename}}/{{version}}/data?userHost={{user_host}}&direct HTTP/1.1
|
||||
```
|
||||
'''
|
||||
config_manager = ConfigurationManager()
|
||||
request_manager = RequestManager(
|
||||
request=request
|
||||
)
|
||||
|
||||
direct = 'direct' in kwargs
|
||||
|
||||
example_url: Optional[ParseResult] = None
|
||||
if direct:
|
||||
example_url = config_manager.example_url()
|
||||
|
||||
base_url = request_manager.resolve_base_url(example_url)
|
||||
source_basename: str = kwargs['source_basename']
|
||||
version: int = kwargs['version']
|
||||
optional_user_host = request.GET.get('userHost')
|
||||
user_host = request_manager.resolve_address(optional_user_host)
|
||||
|
||||
storage_manager = StorageManager(
|
||||
config_manager=config_manager,
|
||||
user_host=user_host,
|
||||
source_basename=source_basename
|
||||
)
|
||||
history_manager = HistoryManager(
|
||||
storage_manager=storage_manager
|
||||
)
|
||||
|
||||
history_data = history_manager.data(
|
||||
base_url,
|
||||
version,
|
||||
user_host,
|
||||
direct
|
||||
)
|
||||
|
||||
if jwtManager.isEnabled():
|
||||
history_data.token = jwtManager.encode(loads(history_data.encode()))
|
||||
|
||||
return HttpResponse(
|
||||
history_data.encode(),
|
||||
content_type='application/json'
|
||||
)
|
||||
|
||||
@http.method(HTTPMethod.GET)
|
||||
def download(self, request: HttpRequest, **kwargs: Any) -> HttpResponseBase:
|
||||
'''
|
||||
```http
|
||||
GET {{base_url}}/history/{{source_basename}}/{{version}}/download/{{basename}}?userHost={{user_host}} HTTP/1.1
|
||||
```
|
||||
'''
|
||||
config_manager = ConfigurationManager()
|
||||
request_manager = RequestManager(
|
||||
request=request
|
||||
)
|
||||
|
||||
source_basename: str = kwargs['source_basename']
|
||||
version: int = kwargs['version']
|
||||
basename: str = kwargs['basename']
|
||||
optional_user_host = request.GET.get('userHost')
|
||||
user_host = request_manager.resolve_address(optional_user_host)
|
||||
|
||||
storage_manager = StorageManager(
|
||||
config_manager=config_manager,
|
||||
user_host=user_host,
|
||||
source_basename=source_basename
|
||||
)
|
||||
history_manager = HistoryManager(
|
||||
storage_manager=storage_manager
|
||||
)
|
||||
|
||||
version_directory = history_manager.version_directory(version)
|
||||
file = version_directory.joinpath(basename)
|
||||
|
||||
# if not file.exists():
|
||||
# return HttpResponse(
|
||||
# '{ "error": "not exists" }',
|
||||
# content_type='application/json'
|
||||
# )
|
||||
|
||||
return FileResponse(
|
||||
open(file, 'rb'),
|
||||
as_attachment=True
|
||||
)
|
||||
|
||||
@http.method(HTTPMethod.PUT)
|
||||
def restore(self, request: HttpRequest, **kwargs: Any) -> HttpResponseBase:
|
||||
'''
|
||||
```http
|
||||
PUT {{base_url}}/history/{{source_basename}}/{{version}}/restore?userHost={{user_host}}&userId={{user_id}} HTTP/1.1
|
||||
```
|
||||
'''
|
||||
config_manager = ConfigurationManager()
|
||||
request_manager = RequestManager(
|
||||
request=request
|
||||
)
|
||||
|
||||
source_basename: str = kwargs['source_basename']
|
||||
version: int = kwargs['version']
|
||||
optional_user_host = request.GET.get('userHost')
|
||||
user_host = request_manager.resolve_address(optional_user_host)
|
||||
user_id = request.GET.get('userId')
|
||||
|
||||
storage_manager = StorageManager(
|
||||
config_manager=config_manager,
|
||||
user_host=user_host,
|
||||
source_basename=source_basename
|
||||
)
|
||||
history_manager = HistoryManager(
|
||||
storage_manager=storage_manager
|
||||
)
|
||||
|
||||
raw_user = find_user(user_id)
|
||||
user = HistoryUser(
|
||||
id=raw_user.id,
|
||||
name=raw_user.name
|
||||
)
|
||||
|
||||
history_manager.restore(version, user)
|
||||
|
||||
return HttpResponse()
|
||||
|
||||
@dataclass
|
||||
class HistoryManager():
|
||||
storage_manager: StorageManager
|
||||
|
||||
# History Management
|
||||
|
||||
def history(self) -> History:
|
||||
history = History(
|
||||
current_version=self.latest_version(),
|
||||
history=[]
|
||||
)
|
||||
|
||||
for version in range(
|
||||
HistoryManager.minimal_version,
|
||||
history.current_version + 1
|
||||
):
|
||||
item = self.item(version)
|
||||
if item is None:
|
||||
continue
|
||||
|
||||
history.history.append(item)
|
||||
|
||||
return history
|
||||
|
||||
# Data Management
|
||||
|
||||
def data(
|
||||
self,
|
||||
base_url: ParseResult,
|
||||
version: int,
|
||||
user_host: str,
|
||||
direct: bool
|
||||
) -> Optional[HistoryData]:
|
||||
key = self.key(version)
|
||||
if key is None:
|
||||
return None
|
||||
|
||||
previous_version = version - 1
|
||||
previous = self.data(
|
||||
base_url,
|
||||
previous_version,
|
||||
user_host,
|
||||
direct
|
||||
)
|
||||
|
||||
history_url = self.history_url(base_url)
|
||||
version_url = self.version_url(history_url, version)
|
||||
|
||||
changes_url: Optional[str] = None
|
||||
if previous is not None:
|
||||
file = self.diff_file(version)
|
||||
download_url = self.download_url(version_url, file.name)
|
||||
personal_url = self.personalize_url(download_url, user_host)
|
||||
changes_url = personal_url.geturl()
|
||||
|
||||
file = self.item_file(version)
|
||||
file_type = file.suffix.replace('.', '')
|
||||
download_url = self.download_url(version_url, file.name)
|
||||
personal_url = self.personalize_url(download_url, user_host)
|
||||
url = personal_url.geturl()
|
||||
|
||||
direct_url: Optional[str] = None
|
||||
if direct:
|
||||
direct_url = download_url.geturl()
|
||||
|
||||
return HistoryData(
|
||||
changes_url=changes_url,
|
||||
file_type=file_type,
|
||||
key=key,
|
||||
previous=previous,
|
||||
token=None,
|
||||
url=url,
|
||||
direct_url=direct_url,
|
||||
version=version
|
||||
)
|
||||
|
||||
def personalize_url(self, url: ParseResult, user_host: str) -> ParseResult:
|
||||
parsed_query = parse_qs(url.query)
|
||||
parsed_query.update({
|
||||
# False positive: the update supports dict.
|
||||
'userHost': user_host # type: ignore # noqa: E261
|
||||
})
|
||||
query = urlencode(parsed_query)
|
||||
return ParseResult(
|
||||
scheme=url.scheme,
|
||||
netloc=url.netloc,
|
||||
path=url.path,
|
||||
params=url.params,
|
||||
query=query,
|
||||
fragment=url.fragment
|
||||
)
|
||||
|
||||
def download_url(self, base_url: ParseResult, basename: str) -> ParseResult:
|
||||
base = base_url.geturl()
|
||||
url = reduce(urljoin, [
|
||||
f'{base}/',
|
||||
'download/',
|
||||
basename
|
||||
])
|
||||
return urlparse(f'{url}')
|
||||
|
||||
def version_url(self, base_url: ParseResult, version: int) -> ParseResult:
|
||||
base = base_url.geturl()
|
||||
url = reduce(urljoin, [
|
||||
f'{base}/',
|
||||
f'{version}'
|
||||
])
|
||||
return urlparse(f'{url}')
|
||||
|
||||
def history_url(self, base_url: ParseResult) -> ParseResult:
|
||||
base = base_url.geturl()
|
||||
source_basename = quote(self.storage_manager.source_basename)
|
||||
url = reduce(urljoin, [
|
||||
f'{base}/',
|
||||
'history/',
|
||||
source_basename
|
||||
])
|
||||
return urlparse(f'{url}')
|
||||
|
||||
# Rejuvenation Management
|
||||
|
||||
# def force_save(self)
|
||||
|
||||
def save(
|
||||
self,
|
||||
changes: HistoryChanges,
|
||||
diff: Iterator[Any],
|
||||
item: Iterator[Any]
|
||||
):
|
||||
version = self.next_version()
|
||||
|
||||
self.bootstrap_key(version)
|
||||
self.write_changes(version, changes)
|
||||
self.write_diff(version, diff)
|
||||
self.write_item(version, item)
|
||||
|
||||
source_file = self.storage_manager.source_file()
|
||||
file = self.item_file(version)
|
||||
copy(f'{file}', f'{source_file}')
|
||||
|
||||
def restore(self, version: int, user: HistoryUser):
|
||||
recovery_file = self.item_file(version)
|
||||
source_file = self.storage_manager.source_file()
|
||||
copy(f'{recovery_file}', f'{source_file}')
|
||||
|
||||
version = self.next_version()
|
||||
self.bootstrap(version, user)
|
||||
|
||||
def bootstrap_initial_item(self, user: HistoryUser):
|
||||
self.bootstrap(HistoryManager.minimal_version, user)
|
||||
|
||||
def bootstrap(self, version: int, user: HistoryUser):
|
||||
self.bootstrap_key(version)
|
||||
self.bootstrap_changes(version, user)
|
||||
self.bootstrap_item(version)
|
||||
|
||||
# Item Management
|
||||
|
||||
def bootstrap_item(self, version: int):
|
||||
source_file = self.storage_manager.source_file()
|
||||
file = self.item_file(version)
|
||||
copy(f'{source_file}', f'{file}')
|
||||
|
||||
def write_item(self, version: int, stream: Iterator[Any]):
|
||||
file = self.item_file(version)
|
||||
with open(f'{file}', 'wb') as output:
|
||||
for chunk in stream:
|
||||
output.write(chunk)
|
||||
|
||||
def item(self, version: int) -> Optional[HistoryItem]:
|
||||
key = self.key(version)
|
||||
if key is None:
|
||||
return None
|
||||
|
||||
changes = self.changes(version)
|
||||
if changes is None:
|
||||
return None
|
||||
|
||||
first_changes = optional.expression(lambda: changes.changes[0])
|
||||
if first_changes is None:
|
||||
return None
|
||||
|
||||
return HistoryItem(
|
||||
changes=changes.changes,
|
||||
created=first_changes.created,
|
||||
key=key,
|
||||
server_version=changes.server_version,
|
||||
user=first_changes.user,
|
||||
version=version
|
||||
)
|
||||
|
||||
def item_file(self, version: int) -> Path:
|
||||
directory = self.version_directory(version)
|
||||
source_file = self.storage_manager.source_file()
|
||||
return directory.joinpath(f'prev{source_file.suffix}')
|
||||
|
||||
# Changes Management
|
||||
|
||||
def bootstrap_changes(self, version: int, user: HistoryUser):
|
||||
changes = HistoryManager.generate_changes(user)
|
||||
self.write_changes(version, changes)
|
||||
|
||||
def write_changes(self, version: int, changes: HistoryChanges):
|
||||
content = changes.encode()
|
||||
file = self.changes_file(version)
|
||||
file.write_text(content, 'utf-8')
|
||||
|
||||
def changes(self, version: int) -> Optional[HistoryChanges]:
|
||||
file = self.changes_file(version)
|
||||
if not file.exists():
|
||||
return None
|
||||
|
||||
content = file.read_text('utf-8')
|
||||
return HistoryChanges.decode(content)
|
||||
|
||||
def changes_file(self, version: int) -> Path:
|
||||
directory = self.version_directory(version)
|
||||
return directory.joinpath('changes.json')
|
||||
|
||||
def write_diff(self, version, stream: Iterator[Any]):
|
||||
file = self.diff_file(version)
|
||||
with open(f'{file}', 'wb') as output:
|
||||
for chunk in stream:
|
||||
output.write(chunk)
|
||||
|
||||
def diff_file(self, version: int) -> Path:
|
||||
directory = self.version_directory(version)
|
||||
return directory.joinpath('diff.zip')
|
||||
|
||||
@classmethod
|
||||
def generate_changes(cls, user: HistoryUser) -> HistoryChanges:
|
||||
today = datetime.today()
|
||||
created = today.strftime('%Y-%m-%d %H:%M:%S')
|
||||
item = HistoryChangesItem(
|
||||
created=created,
|
||||
user=user
|
||||
)
|
||||
return HistoryChanges(
|
||||
server_version=None,
|
||||
changes=[
|
||||
item
|
||||
]
|
||||
)
|
||||
|
||||
# Key Management
|
||||
|
||||
def bootstrap_key(self, version: int):
|
||||
key = HistoryManager.generate_key()
|
||||
self.write_key(version, key)
|
||||
|
||||
def write_key(self, version: int, key: str):
|
||||
file = self.key_file(version)
|
||||
file.write_text(key, 'utf-8')
|
||||
|
||||
def key(self, version: int) -> Optional[str]:
|
||||
file = self.key_file(version)
|
||||
if not file.exists():
|
||||
return None
|
||||
|
||||
content = file.read_text('utf-8')
|
||||
return content
|
||||
|
||||
def key_file(self, version: int) -> Path:
|
||||
directory = self.version_directory(version)
|
||||
return directory.joinpath('key.txt')
|
||||
|
||||
@classmethod
|
||||
def generate_key(cls) -> str:
|
||||
key = uuid1()
|
||||
return f'{key}'
|
||||
|
||||
# Version Management
|
||||
|
||||
# def version_file(self, version: int, basename: str) -> Path
|
||||
|
||||
def version_directory(self, version: int) -> Path:
|
||||
parent_directory = self.history_directory()
|
||||
directory = parent_directory.joinpath(f'{version}')
|
||||
if not directory.exists():
|
||||
directory.mkdir()
|
||||
return directory
|
||||
|
||||
# Storage Management
|
||||
|
||||
minimal_version = 1
|
||||
|
||||
def next_version(self) -> int:
|
||||
version = self.latest_version()
|
||||
return version + 1
|
||||
|
||||
def latest_version(self) -> int:
|
||||
directory = self.history_directory()
|
||||
version = 0
|
||||
|
||||
for file in directory.iterdir():
|
||||
if not file.is_dir():
|
||||
continue
|
||||
|
||||
if not len(list(file.iterdir())) > 0:
|
||||
continue
|
||||
|
||||
version += 1
|
||||
|
||||
return version
|
||||
|
||||
def history_directory(self) -> Path:
|
||||
file = self.storage_manager.source_file()
|
||||
directory = file.parent.joinpath(f'{file.name}-hist')
|
||||
if not directory.exists():
|
||||
directory.mkdir()
|
||||
return directory
|
||||
19
web/documentserver-example/python/src/proxy/__init__.py
Normal file
19
web/documentserver-example/python/src/proxy/__init__.py
Normal file
@ -0,0 +1,19 @@
|
||||
"""
|
||||
|
||||
(c) Copyright Ascensio System SIA 2023
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
"""
|
||||
|
||||
from .proxy import *
|
||||
53
web/documentserver-example/python/src/proxy/proxy.py
Normal file
53
web/documentserver-example/python/src/proxy/proxy.py
Normal file
@ -0,0 +1,53 @@
|
||||
"""
|
||||
|
||||
(c) Copyright Ascensio System SIA 2023
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
"""
|
||||
|
||||
# pylint: disable=missing-class-docstring
|
||||
# pylint: disable=missing-function-docstring
|
||||
|
||||
from dataclasses import dataclass
|
||||
from urllib.parse import ParseResult, urlparse
|
||||
from src.configuration import ConfigurationManager
|
||||
|
||||
@dataclass
|
||||
class ProxyManager():
|
||||
config_manager: ConfigurationManager
|
||||
|
||||
def resolve_document_server_url(self, url: str) -> ParseResult:
|
||||
parsed_url = urlparse(url)
|
||||
if not self.__refer_document_server_public_url(parsed_url):
|
||||
return parsed_url
|
||||
return self.__redirect_document_server_public_url(parsed_url)
|
||||
|
||||
def __refer_document_server_public_url(self, url: ParseResult) -> bool:
|
||||
public_url = self.config_manager.document_server_public_url()
|
||||
return (
|
||||
url.scheme == public_url.scheme and
|
||||
url.hostname == public_url.hostname and
|
||||
url.port == public_url.port
|
||||
)
|
||||
|
||||
def __redirect_document_server_public_url(self, url: ParseResult) -> ParseResult:
|
||||
private_url = self.config_manager.document_server_private_url()
|
||||
return ParseResult(
|
||||
scheme=private_url.scheme,
|
||||
netloc=f'{private_url.hostname}:{private_url.port}',
|
||||
path=url.path,
|
||||
params=url.params,
|
||||
query=url.query,
|
||||
fragment=url.fragment
|
||||
)
|
||||
66
web/documentserver-example/python/src/proxy/proxy_tests.py
Normal file
66
web/documentserver-example/python/src/proxy/proxy_tests.py
Normal file
@ -0,0 +1,66 @@
|
||||
"""
|
||||
|
||||
(c) Copyright Ascensio System SIA 2023
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
"""
|
||||
|
||||
# pylint: disable=missing-class-docstring
|
||||
# pylint: disable=missing-function-docstring
|
||||
|
||||
from unittest import TestCase
|
||||
from unittest.mock import patch
|
||||
from urllib.parse import urlparse
|
||||
from src.configuration import ConfigurationManager
|
||||
from . import ProxyManager
|
||||
|
||||
class ProxyManagerTests(TestCase):
|
||||
@patch.object(
|
||||
ConfigurationManager,
|
||||
'document_server_public_url',
|
||||
return_value=urlparse('http://localhost:3000')
|
||||
)
|
||||
@patch.object(
|
||||
ConfigurationManager,
|
||||
'document_server_private_url',
|
||||
return_value=urlparse('http://proxy:3001')
|
||||
)
|
||||
def test_resolves_a_url_that_refers_to_the_document_server_public_url(self, *_):
|
||||
config_manager = ConfigurationManager()
|
||||
proxy_manager = ProxyManager(config_manager)
|
||||
|
||||
url = 'http://localhost:3000/endpoint?query=string'
|
||||
resolved_url = proxy_manager.resolve_document_server_url(url)
|
||||
|
||||
self.assertEqual(
|
||||
resolved_url.geturl(),
|
||||
'http://proxy:3001/endpoint?query=string'
|
||||
)
|
||||
|
||||
@patch.object(
|
||||
ConfigurationManager,
|
||||
'document_server_public_url',
|
||||
return_value=urlparse('http://localhost:3000')
|
||||
)
|
||||
def test_resolves_a_url_that_does_not_refers_to_the_document_server_public_url(self, _):
|
||||
config_manager = ConfigurationManager()
|
||||
proxy_manager = ProxyManager(config_manager)
|
||||
|
||||
url = 'http://localhost:8080/endpoint?query=string'
|
||||
resolved_url = proxy_manager.resolve_document_server_url(url)
|
||||
|
||||
self.assertEqual(
|
||||
resolved_url.geturl(),
|
||||
'http://localhost:8080/endpoint?query=string'
|
||||
)
|
||||
17
web/documentserver-example/python/src/request/__init__.py
Normal file
17
web/documentserver-example/python/src/request/__init__.py
Normal file
@ -0,0 +1,17 @@
|
||||
#
|
||||
# (c) Copyright Ascensio System SIA 2023
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
from .request import *
|
||||
57
web/documentserver-example/python/src/request/request.py
Normal file
57
web/documentserver-example/python/src/request/request.py
Normal file
@ -0,0 +1,57 @@
|
||||
#
|
||||
# (c) Copyright Ascensio System SIA 2023
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
from dataclasses import dataclass
|
||||
from re import sub
|
||||
from typing import Optional
|
||||
from urllib.parse import ParseResult
|
||||
from django.http import HttpRequest
|
||||
|
||||
@dataclass
|
||||
class RequestManager():
|
||||
request: HttpRequest
|
||||
|
||||
def resolve_base_url(
|
||||
self,
|
||||
base_url: Optional[ParseResult] = None
|
||||
) -> ParseResult:
|
||||
return base_url or self.__base_url()
|
||||
|
||||
def __base_url(self):
|
||||
scheme = (
|
||||
self.request.headers.get('X-Forwarded-Proto') or
|
||||
self.request.scheme or
|
||||
'http'
|
||||
)
|
||||
netloc = self.request.get_host()
|
||||
return ParseResult(
|
||||
scheme=scheme,
|
||||
netloc=netloc,
|
||||
path='',
|
||||
params='',
|
||||
query='',
|
||||
fragment=''
|
||||
)
|
||||
|
||||
def resolve_address(self, address: Optional[str] = None) -> str:
|
||||
raw = address or self.__address()
|
||||
return sub(r'[^0-9\-.a-zA-Z_=]', '_', raw)
|
||||
|
||||
def __address(self) -> str:
|
||||
forwarded = self.request.headers.get('X-Forwarded-For')
|
||||
if forwarded:
|
||||
return forwarded.split(',')[0]
|
||||
return self.request.META['REMOTE_ADDR']
|
||||
@ -1,103 +0,0 @@
|
||||
"""
|
||||
Django settings for example project.
|
||||
|
||||
Generated by 'django-admin startproject' using Django 2.2.6.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/3.1/topics/settings/
|
||||
|
||||
For the full list of settings and their values, see
|
||||
https://docs.djangoproject.com/en/3.1/ref/settings/
|
||||
"""
|
||||
|
||||
import os
|
||||
import config
|
||||
import mimetypes
|
||||
|
||||
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
|
||||
# Quick-start development settings - unsuitable for production
|
||||
# See https://docs.djangoproject.com/en/3.1/howto/deployment/checklist/
|
||||
|
||||
# SECURITY WARNING: keep the secret key used in production secret!
|
||||
SECRET_KEY = '7a5qnm_bv)iskjhx%4cbwwdmjev03%zewm=3@4s*uz)el#ds5o'
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = True
|
||||
|
||||
ALLOWED_HOSTS = [
|
||||
'*'
|
||||
]
|
||||
|
||||
X_FRAME_OPTIONS = 'ALLOWALL'
|
||||
XS_SHARING_ALLOWED_METHODS = ['GET']
|
||||
|
||||
# Application definition
|
||||
|
||||
INSTALLED_APPS = [
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.sessions',
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
'src.utils.historyManager.CorsHeaderMiddleware',
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
]
|
||||
|
||||
ROOT_URLCONF = 'src.urls'
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||
'DIRS': [ 'templates' ],
|
||||
'APP_DIRS': True,
|
||||
'OPTIONS': {
|
||||
'context_processors': [
|
||||
'django.template.context_processors.debug',
|
||||
'django.template.context_processors.request',
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
WSGI_APPLICATION = 'src.wsgi.application'
|
||||
|
||||
|
||||
# Database
|
||||
# https://docs.djangoproject.com/en/3.1/ref/settings/#databases
|
||||
|
||||
DATABASES = {}
|
||||
|
||||
|
||||
# Password validation
|
||||
# https://docs.djangoproject.com/en/3.1/ref/settings/#auth-password-validators
|
||||
|
||||
AUTH_PASSWORD_VALIDATORS = []
|
||||
|
||||
|
||||
# Internationalization
|
||||
# https://docs.djangoproject.com/en/3.1/topics/i18n/
|
||||
|
||||
LANGUAGE_CODE = 'en-us'
|
||||
|
||||
TIME_ZONE = 'UTC'
|
||||
|
||||
USE_I18N = True
|
||||
|
||||
USE_L10N = True
|
||||
|
||||
USE_TZ = True
|
||||
|
||||
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
# https://docs.djangoproject.com/en/3.1/howto/static-files/
|
||||
|
||||
mimetypes.add_type("text/javascript", ".js", True)
|
||||
STATIC_ROOT = ''
|
||||
STATIC_URL = '/static/'
|
||||
STATICFILES_DIRS = ( os.path.join('static'), os.path.join(config.STORAGE_PATH), os.path.join('assets/sample'))
|
||||
17
web/documentserver-example/python/src/storage/__init__.py
Normal file
17
web/documentserver-example/python/src/storage/__init__.py
Normal file
@ -0,0 +1,17 @@
|
||||
#
|
||||
# (c) Copyright Ascensio System SIA 2023
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
from .storage import StorageManager
|
||||
42
web/documentserver-example/python/src/storage/storage.py
Normal file
42
web/documentserver-example/python/src/storage/storage.py
Normal file
@ -0,0 +1,42 @@
|
||||
#
|
||||
# (c) Copyright Ascensio System SIA 2023
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from src.configuration import ConfigurationManager
|
||||
|
||||
@dataclass
|
||||
class StorageManager():
|
||||
config_manager: ConfigurationManager
|
||||
user_host: str
|
||||
source_basename: str
|
||||
|
||||
def source_file(self) -> Path:
|
||||
directory = self.user_directory()
|
||||
return directory.joinpath(self.source_basename)
|
||||
|
||||
def user_directory(self) -> Path:
|
||||
parent_directory = self.storage_directory()
|
||||
directory = parent_directory.joinpath(self.user_host)
|
||||
if not directory.exists():
|
||||
directory.mkdir()
|
||||
return directory
|
||||
|
||||
def storage_directory(self) -> Path:
|
||||
directory = self.config_manager.storage_path()
|
||||
if not directory.exists():
|
||||
directory.mkdir()
|
||||
return directory
|
||||
@ -1,41 +0,0 @@
|
||||
"""
|
||||
|
||||
(c) Copyright Ascensio System SIA 2023
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
"""
|
||||
|
||||
from django.urls import path, re_path
|
||||
|
||||
from src.views import index, actions
|
||||
from django.contrib.staticfiles.urls import staticfiles_urlpatterns
|
||||
|
||||
urlpatterns = [
|
||||
path('', index.default),
|
||||
path('upload', actions.upload),
|
||||
path('download', actions.download),
|
||||
path('downloadhistory', actions.downloadhistory),
|
||||
path('convert', actions.convert),
|
||||
path('create', actions.createNew),
|
||||
path('edit', actions.edit),
|
||||
path('track', actions.track),
|
||||
path('remove', actions.remove),
|
||||
path('csv', actions.csv),
|
||||
path('files', actions.files),
|
||||
path('saveas', actions.saveAs),
|
||||
path('rename', actions.rename),
|
||||
path('reference', actions.reference)
|
||||
]
|
||||
|
||||
urlpatterns += staticfiles_urlpatterns()
|
||||
@ -17,7 +17,6 @@
|
||||
"""
|
||||
|
||||
|
||||
import config
|
||||
import os
|
||||
import shutil
|
||||
import io
|
||||
@ -27,24 +26,30 @@ import time
|
||||
import urllib.parse
|
||||
import magic
|
||||
|
||||
from uuid import uuid1
|
||||
from django.conf import settings
|
||||
from django.http import HttpResponse, HttpResponseRedirect, FileResponse
|
||||
from src import settings
|
||||
from . import fileUtils, historyManager
|
||||
from src.configuration import ConfigurationManager
|
||||
|
||||
def isCanFillForms(ext):
|
||||
return ext in config.DOC_SERV_FILLFORMS
|
||||
config = ConfigurationManager()
|
||||
return ext in config.fillable_file_extensions()
|
||||
|
||||
# check if the file extension can be viewed
|
||||
def isCanView(ext):
|
||||
return ext in config.DOC_SERV_VIEWED
|
||||
config = ConfigurationManager()
|
||||
return ext in config.viewable_file_extensions()
|
||||
|
||||
# check if the file extension can be edited
|
||||
def isCanEdit(ext):
|
||||
return ext in config.DOC_SERV_EDITED
|
||||
config = ConfigurationManager()
|
||||
return ext in config.editable_file_extensions()
|
||||
|
||||
# check if the file extension can be converted
|
||||
def isCanConvert(ext):
|
||||
return ext in config.DOC_SERV_CONVERT
|
||||
config = ConfigurationManager()
|
||||
return ext in config.convertible_file_extensions()
|
||||
|
||||
# check if the file extension is supported by the editor (it can be viewed or edited or converted)
|
||||
def isSupportedExt(ext):
|
||||
@ -87,8 +92,10 @@ def getCorrectName(filename, req):
|
||||
|
||||
# get server url
|
||||
def getServerUrl (forDocumentServer, req):
|
||||
if (forDocumentServer and config.EXAMPLE_DOMAIN is not None):
|
||||
return config.EXAMPLE_DOMAIN
|
||||
config = ConfigurationManager()
|
||||
example_url = config.example_url()
|
||||
if (forDocumentServer and example_url is not None):
|
||||
return example_url.geturl()
|
||||
else:
|
||||
return req.headers.get("x-forwarded-proto") or req.scheme + "://" + req.get_host()
|
||||
|
||||
@ -122,7 +129,8 @@ def getRootFolder(req):
|
||||
else:
|
||||
curAdr = req.META['REMOTE_ADDR']
|
||||
|
||||
directory = config.STORAGE_PATH if os.path.isabs(config.STORAGE_PATH) else os.path.join(config.STORAGE_PATH, curAdr)
|
||||
config = ConfigurationManager()
|
||||
directory = config.storage_path().joinpath(curAdr)
|
||||
|
||||
if not os.path.exists(directory): # if such a directory does not exist, make it
|
||||
os.makedirs(directory)
|
||||
@ -136,7 +144,8 @@ def getHistoryPath(filename, file, version, req):
|
||||
else:
|
||||
curAdr = req.META['REMOTE_ADDR']
|
||||
|
||||
directory = os.path.join(config.STORAGE_PATH, curAdr)
|
||||
config = ConfigurationManager()
|
||||
directory = config.storage_path().joinpath(curAdr)
|
||||
if not os.path.exists(directory): # the directory with host address doesn't exist
|
||||
filePath = os.path.join(getRootFolder(req), f'{filename}-hist', version, file)
|
||||
else:
|
||||
@ -157,10 +166,11 @@ def getForcesavePath(filename, req, create):
|
||||
else:
|
||||
curAdr = req.META['REMOTE_ADDR']
|
||||
|
||||
directory = os.path.join(config.STORAGE_PATH, curAdr)
|
||||
config = ConfigurationManager()
|
||||
directory = config.storage_path().joinpath(curAdr)
|
||||
if not os.path.exists(directory): # the directory with host address doesn't exist
|
||||
return ""
|
||||
|
||||
|
||||
directory = os.path.join(directory, f'{filename}-hist') # get the path to the history of the given file
|
||||
if (not os.path.exists(directory)):
|
||||
if create: # if the history directory doesn't exist
|
||||
@ -208,9 +218,10 @@ def saveFile(response, path):
|
||||
file.write(chunk)
|
||||
return
|
||||
|
||||
# download file from the given url
|
||||
# download file from the given url
|
||||
def downloadFileFromUri(uri, path = None, withSave = False):
|
||||
resp = requests.get(uri, stream=True, verify = config.DOC_SERV_VERIFY_PEER, timeout=5)
|
||||
config = ConfigurationManager()
|
||||
resp = requests.get(uri, stream=True, verify = config.ssl_verify_peer_mode_enabled(), timeout=5)
|
||||
status_code = resp.status_code
|
||||
if status_code != 200: # checking status code
|
||||
raise RuntimeError('Document editing service returned status: %s' % status_code)
|
||||
@ -227,7 +238,7 @@ def createSample(fileType, sample, req):
|
||||
if not sample:
|
||||
sample = 'false'
|
||||
|
||||
sampleName = 'sample' if sample == 'true' else 'new' # create sample or new template
|
||||
sampleName = 'sample' if sample == 'true' else 'new' # create sample or new template
|
||||
|
||||
filename = getCorrectName(f'{sampleName}{ext}', req) # get file name with an index if such a file name already exists
|
||||
path = getStoragePath(filename, req)
|
||||
@ -247,13 +258,8 @@ def removeFile(filename, req):
|
||||
|
||||
# generate file key
|
||||
def generateFileKey(filename, req):
|
||||
path = getStoragePath(filename, req)
|
||||
uri = getFileUri(filename, False, req)
|
||||
stat = os.stat(path) # get the directory parameters
|
||||
|
||||
h = str(hash(f'{uri}_{stat.st_mtime_ns}')) # get the hash value of the file url and the date of its last modification and turn it into a string format
|
||||
replaced = re.sub(r'[^0-9-.a-zA-Z_=]', '_', h)
|
||||
return replaced[:20] # take the first 20 characters for the key
|
||||
key = uuid1()
|
||||
return f'{key}'
|
||||
|
||||
# generate the document key value
|
||||
def generateRevisionId(expectedKey):
|
||||
@ -273,7 +279,7 @@ def getFilesInfo(req):
|
||||
stats = os.stat(os.path.join(getRootFolder(req), f.get("title"))) # get file information
|
||||
result.append( # write file parameters to the file object
|
||||
{ "version" : historyManager.getFileVersion(historyManager.getHistoryDir(getStoragePath(f.get("title"), req))),
|
||||
"id" : generateFileKey(f.get("title"), req),
|
||||
"id" : generateFileKey(f.get("title"), req),
|
||||
"contentLength" : "%.2f KB" % (stats.st_size/1024),
|
||||
"pureContentLength" : stats.st_size,
|
||||
"title" : f.get("title"),
|
||||
@ -285,7 +291,7 @@ def getFilesInfo(req):
|
||||
|
||||
if fileId :
|
||||
if len(resultID) > 0 : return resultID
|
||||
else : return "File not found"
|
||||
else : return "File not found"
|
||||
else :
|
||||
return result
|
||||
|
||||
@ -295,4 +301,4 @@ def download(filePath):
|
||||
response['Content-Length'] = os.path.getsize(filePath)
|
||||
response['Content-Disposition'] = "attachment;filename*=UTF-8\'\'" + urllib.parse.unquote(os.path.basename(filePath))
|
||||
response['Content-Type'] = magic.from_file(filePath, mime=True)
|
||||
return response
|
||||
return response
|
||||
|
||||
@ -16,7 +16,7 @@
|
||||
|
||||
"""
|
||||
|
||||
import config
|
||||
from src.configuration import ConfigurationManager
|
||||
|
||||
# get file name from the document url
|
||||
def getFileName(str):
|
||||
@ -37,12 +37,13 @@ def getFileExt(str):
|
||||
|
||||
# get file type
|
||||
def getFileType(str):
|
||||
config = ConfigurationManager()
|
||||
ext = getFileExt(str)
|
||||
if ext in config.EXT_DOCUMENT:
|
||||
if ext in config.document_file_extensions():
|
||||
return 'word'
|
||||
if ext in config.EXT_SPREADSHEET:
|
||||
if ext in config.spreadsheet_file_extensions():
|
||||
return 'cell'
|
||||
if ext in config.EXT_PRESENTATION:
|
||||
if ext in config.presentation_file_extensions():
|
||||
return 'slide'
|
||||
|
||||
return 'word' # default file type is word
|
||||
@ -17,23 +17,26 @@
|
||||
"""
|
||||
|
||||
import os
|
||||
import io
|
||||
import json
|
||||
import config
|
||||
from pathlib import Path
|
||||
from src.configuration import ConfigurationManager
|
||||
from src.history import HistoryManager, HistoryUser
|
||||
from src.common import optional
|
||||
from src.storage import StorageManager
|
||||
from src.utils import users
|
||||
|
||||
from . import users, fileUtils
|
||||
from datetime import datetime
|
||||
from src import settings
|
||||
from src.utils import docManager
|
||||
from src.utils import jwtManager
|
||||
|
||||
# get the path to the history direction
|
||||
def getHistoryDir(storagePath):
|
||||
return f'{storagePath}-hist'
|
||||
|
||||
# get the path to the given file version
|
||||
def getVersionDir(histDir, version):
|
||||
return os.path.join(histDir, str(version))
|
||||
source_file = Path(storagePath)
|
||||
config_manager = ConfigurationManager()
|
||||
storage_manager = StorageManager(
|
||||
config_manager=config_manager,
|
||||
user_host=source_file.parent.name,
|
||||
source_basename=source_file.name
|
||||
)
|
||||
history_manager = HistoryManager(
|
||||
storage_manager=storage_manager
|
||||
)
|
||||
directory = history_manager.history_directory()
|
||||
return f'{directory}'
|
||||
|
||||
# get file version of the given history directory
|
||||
def getFileVersion(histDir):
|
||||
@ -43,188 +46,75 @@ def getFileVersion(histDir):
|
||||
cnt = 1
|
||||
|
||||
for f in os.listdir(histDir): # run through all the files in the history directory
|
||||
if not os.path.isfile(os.path.join(histDir, f)): # and count the number of files
|
||||
cnt += 1
|
||||
|
||||
path = os.path.join(histDir, f)
|
||||
directory = Path(path)
|
||||
|
||||
if not directory.is_dir():
|
||||
continue
|
||||
|
||||
if not len(list(directory.iterdir())) > 0:
|
||||
continue
|
||||
|
||||
cnt += 1
|
||||
|
||||
return cnt
|
||||
|
||||
# get the path to the next file version
|
||||
def getNextVersionDir(histDir):
|
||||
v = getFileVersion(histDir) # get file version of the given history directory
|
||||
path = getVersionDir(histDir, v) # get the path to the next file version
|
||||
|
||||
if not os.path.exists(path): # if this path doesn't exist
|
||||
os.makedirs(path) # make the directory for this file version
|
||||
return path
|
||||
|
||||
# get the path to a file archive with differences in the given file version
|
||||
def getChangesZipPath(verDir):
|
||||
return os.path.join(verDir, 'diff.zip')
|
||||
|
||||
# get the path to a json file with changes of the given file version
|
||||
def getChangesHistoryPath(verDir):
|
||||
return os.path.join(verDir, 'changes.json')
|
||||
|
||||
# get the path to the previous file version
|
||||
def getPrevFilePath(verDir, ext):
|
||||
return os.path.join(verDir, f'prev{ext}')
|
||||
|
||||
# get the path to a txt file with a key information in it
|
||||
def getKeyPath(verDir):
|
||||
return os.path.join(verDir, 'key.txt')
|
||||
|
||||
# get the path to a json file with meta data about this file
|
||||
def getMetaPath(histDir):
|
||||
return os.path.join(histDir, 'createdInfo.json')
|
||||
|
||||
# create a json file with file meta data using the storage path and request
|
||||
def createMeta(storagePath, req):
|
||||
histDir = getHistoryDir(storagePath)
|
||||
path = getMetaPath(histDir) # get the path to a json file with meta data about file
|
||||
source_file = Path(storagePath)
|
||||
config_manager = ConfigurationManager()
|
||||
storage_manager = StorageManager(
|
||||
config_manager=config_manager,
|
||||
user_host=source_file.parent.name,
|
||||
source_basename=source_file.name
|
||||
)
|
||||
history_manager = HistoryManager(
|
||||
storage_manager=storage_manager
|
||||
)
|
||||
raw_user = users.getUserFromReq(req)
|
||||
user = HistoryUser(
|
||||
id=raw_user.id,
|
||||
name=raw_user.name
|
||||
)
|
||||
history_manager.bootstrap_initial_item(user)
|
||||
|
||||
if not os.path.exists(histDir):
|
||||
os.makedirs(histDir)
|
||||
|
||||
user = users.getUserFromReq(req) # get the user information (id and name)
|
||||
|
||||
obj = { # create the meta data object
|
||||
'created': datetime.today().strftime('%Y-%m-%d %H:%M:%S'),
|
||||
'uid': user.id,
|
||||
'uname': user.name
|
||||
}
|
||||
|
||||
writeFile(path, json.dumps(obj))
|
||||
|
||||
return
|
||||
|
||||
# create a json file with file meta data using the file name, user id, user name and user address
|
||||
def createMetaData(filename, uid, uname, usAddr):
|
||||
histDir = getHistoryDir(docManager.getStoragePath(filename, usAddr))
|
||||
path = getMetaPath(histDir) # get the path to a json file with meta data about file
|
||||
config_manager = ConfigurationManager()
|
||||
storage_manager = StorageManager(
|
||||
config_manager=config_manager,
|
||||
user_host=usAddr,
|
||||
source_basename=filename
|
||||
)
|
||||
history_manager = HistoryManager(
|
||||
storage_manager=storage_manager
|
||||
)
|
||||
user = HistoryUser(
|
||||
id=uid,
|
||||
name=uname
|
||||
)
|
||||
history_manager.bootstrap_initial_item(user)
|
||||
|
||||
if not os.path.exists(histDir):
|
||||
os.makedirs(histDir)
|
||||
|
||||
obj = { # create the meta data object
|
||||
'created': datetime.today().strftime('%Y-%m-%d %H:%M:%S'),
|
||||
'uid': uid,
|
||||
'uname': uname
|
||||
}
|
||||
|
||||
writeFile(path, json.dumps(obj))
|
||||
|
||||
return
|
||||
|
||||
# create file with a given content in it
|
||||
def writeFile(path, content):
|
||||
with io.open(path, 'w') as out:
|
||||
out.write(content)
|
||||
return
|
||||
|
||||
# read a file
|
||||
def readFile(path):
|
||||
with io.open(path, 'r') as stream:
|
||||
return stream.read()
|
||||
|
||||
# get the url to the history file version with a given extension
|
||||
def getPublicHistUri(filename, ver, file, req, isServerUrl=True):
|
||||
host = docManager.getServerUrl(isServerUrl, req)
|
||||
curAdr = f'&userAddress={req.META["REMOTE_ADDR"]}' if isServerUrl else ''
|
||||
return f'{host}/downloadhistory?fileName={filename}&ver={ver}&file={file}{curAdr}'
|
||||
|
||||
# get the meta data of the file
|
||||
def getMeta(storagePath):
|
||||
histDir = getHistoryDir(storagePath)
|
||||
path = getMetaPath(histDir)
|
||||
source_file = Path(storagePath)
|
||||
config_manager = ConfigurationManager()
|
||||
storage_manager = StorageManager(
|
||||
config_manager=config_manager,
|
||||
user_host=source_file.parent.name,
|
||||
source_basename=source_file.name
|
||||
)
|
||||
history_manager = HistoryManager(
|
||||
storage_manager=storage_manager
|
||||
)
|
||||
|
||||
if os.path.exists(path): # check if the json file with file meta data exists
|
||||
with io.open(path, 'r') as stream:
|
||||
return json.loads(stream.read()) # turn meta data into python format
|
||||
|
||||
return None
|
||||
changes = history_manager.changes(HistoryManager.minimal_version)
|
||||
if changes is None:
|
||||
return None
|
||||
|
||||
# get the document history of a given file
|
||||
def getHistoryObject(storagePath, filename, docKey, docUrl, isEnableDirectUrl, req):
|
||||
histDir = getHistoryDir(storagePath)
|
||||
version = getFileVersion(histDir)
|
||||
if version > 0: # if the file was modified (the file version is greater than 0)
|
||||
hist = []
|
||||
histData = {}
|
||||
|
||||
for i in range(1, version + 1): # run through all the file versions
|
||||
obj = {}
|
||||
dataObj = {}
|
||||
prevVerDir = getVersionDir(histDir, i - 1) # get the path to the previous file version
|
||||
verDir = getVersionDir(histDir, i) # get the path to the given file version
|
||||
first_changes = optional.expression(lambda: changes.changes[0])
|
||||
if first_changes is None:
|
||||
return None
|
||||
|
||||
try:
|
||||
key = docKey if i == version else readFile(getKeyPath(verDir)) # get document key
|
||||
|
||||
obj['key'] = key
|
||||
obj['version'] = i
|
||||
dataObj['fileType'] = fileUtils.getFileExt(filename)[1:]
|
||||
dataObj['key'] = key
|
||||
dataObj['version'] = i
|
||||
|
||||
if i == 1: # check if the version number is equal to 1
|
||||
meta = getMeta(storagePath) # get meta data of this file
|
||||
if meta: # write meta information to the object (user information and creation date)
|
||||
obj['created'] = meta['created']
|
||||
obj['user'] = {
|
||||
'id': meta['uid'],
|
||||
'name': meta['uname']
|
||||
}
|
||||
|
||||
dataObj['url'] = docUrl if i == version else getPublicHistUri(filename, i, "prev" + fileUtils.getFileExt(filename), req) # write file url to the data object
|
||||
if isEnableDirectUrl:
|
||||
dataObj['directUrl'] = docManager.getDownloadUrl(filename, req, False) if i == version else getPublicHistUri(filename, i, "prev" + fileUtils.getFileExt(filename), req, False) # write file direct url to the data object
|
||||
|
||||
if i > 1: # check if the version number is greater than 1 (the file was modified)
|
||||
changes = json.loads(readFile(getChangesHistoryPath(prevVerDir))) # get the path to the changes.json file
|
||||
change = changes['changes'][0]
|
||||
|
||||
obj['changes'] = changes['changes'] if change else None # write information about changes to the object
|
||||
obj['serverVersion'] = changes['serverVersion']
|
||||
obj['created'] = change['created'] if change else None
|
||||
obj['user'] = change['user'] if change else None
|
||||
|
||||
prev = histData[str(i - 2)] # get the history data from the previous file version
|
||||
prevInfo = { # write key and url information about previous file version
|
||||
'fileType': prev['fileType'],
|
||||
'key': prev['key'],
|
||||
'url': prev['url'],
|
||||
'directUrl': prev['directUrl']
|
||||
} if isEnableDirectUrl else { # write key and url information about previous file version
|
||||
'fileType': prev['fileType'],
|
||||
'key': prev['key'],
|
||||
'url': prev['url']
|
||||
}
|
||||
dataObj['previous'] = prevInfo # write information about previous file version to the data object
|
||||
dataObj['changesUrl'] = getPublicHistUri(filename, i - 1, "diff.zip", req) # write the path to the diff.zip archive with differences in this file version
|
||||
|
||||
if jwtManager.isEnabled():
|
||||
dataObj['token'] = jwtManager.encode(dataObj)
|
||||
|
||||
hist.append(obj) # add object dictionary to the hist list
|
||||
histData[str(i - 1)] = dataObj # write data object information to the history data
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
histObj = { # write history information about the current file version to the history object
|
||||
'currentVersion': version,
|
||||
'history': hist
|
||||
}
|
||||
|
||||
return { 'history': histObj, 'historyData': histData }
|
||||
return {}
|
||||
|
||||
|
||||
class CorsHeaderMiddleware:
|
||||
def __init__(self, get_response):
|
||||
self.get_response = get_response
|
||||
|
||||
def __call__(self, request):
|
||||
resp = self.get_response(request)
|
||||
if request.path == '/downloadhistory':
|
||||
resp['Access-Control-Allow-Origin'] = config.DOC_SERV_SITE_URL[0:-1]
|
||||
return resp
|
||||
return {
|
||||
'created': first_changes.created,
|
||||
'uid': first_changes.user.id,
|
||||
'uname': first_changes.user.name
|
||||
}
|
||||
|
||||
@ -16,21 +16,25 @@
|
||||
|
||||
"""
|
||||
|
||||
import config
|
||||
import jwt
|
||||
from src.configuration import ConfigurationManager
|
||||
|
||||
# check if a secret key to generate token exists or not
|
||||
def isEnabled():
|
||||
return bool(config.DOC_SERV_JWT_SECRET)
|
||||
config = ConfigurationManager()
|
||||
return bool(config.jwt_secret())
|
||||
|
||||
# check if a secret key to generate token exists or not
|
||||
def useForRequest():
|
||||
return bool(config.DOC_SERV_JWT_USE_FOR_REQUEST)
|
||||
config = ConfigurationManager()
|
||||
return config.jwt_use_for_request()
|
||||
|
||||
# encode a payload object into a token using a secret key and decodes it into the utf-8 format
|
||||
def encode(payload):
|
||||
return jwt.encode(payload, config.DOC_SERV_JWT_SECRET, algorithm='HS256')
|
||||
config = ConfigurationManager()
|
||||
return jwt.encode(payload, config.jwt_secret(), algorithm='HS256')
|
||||
|
||||
# decode a token into a payload object using a secret key
|
||||
def decode(string):
|
||||
return jwt.decode(string, config.DOC_SERV_JWT_SECRET, algorithms=['HS256'])
|
||||
config = ConfigurationManager()
|
||||
return jwt.decode(string, config.jwt_secret(), algorithms=['HS256'])
|
||||
|
||||
@ -18,8 +18,8 @@
|
||||
|
||||
import json
|
||||
import requests
|
||||
import config
|
||||
|
||||
from src.configuration import ConfigurationManager
|
||||
from . import fileUtils, jwtManager
|
||||
|
||||
# convert file and give url to a new file
|
||||
@ -44,13 +44,14 @@ def getConvertedData(docUri, fromExt, toExt, docKey, isAsync, filePass = None, l
|
||||
if (isAsync): # check if the operation is asynchronous
|
||||
payload.setdefault('async', True) # and write this information to the payload object
|
||||
|
||||
config = ConfigurationManager()
|
||||
|
||||
if (jwtManager.isEnabled() and jwtManager.useForRequest()): # check if a secret key to generate token exists or not
|
||||
jwtHeader = 'Authorization' if config.DOC_SERV_JWT_HEADER is None or config.DOC_SERV_JWT_HEADER == '' else config.DOC_SERV_JWT_HEADER # get jwt header
|
||||
headerToken = jwtManager.encode({'payload': payload}) # encode a payload object into a header token
|
||||
payload['token'] = jwtManager.encode(payload) # encode a payload object into a body token
|
||||
headers[jwtHeader] = f'Bearer {headerToken}' # add a header Authorization with a header token with Authorization prefix in it
|
||||
headers[config.jwt_header()] = f'Bearer {headerToken}' # add a header Authorization with a header token with Authorization prefix in it
|
||||
|
||||
response = requests.post(config.DOC_SERV_SITE_URL + config.DOC_SERV_CONVERTER_URL, json=payload, headers=headers, verify = config.DOC_SERV_VERIFY_PEER, timeout=5) # send the headers and body values to the converter and write the result to the response
|
||||
response = requests.post(config.document_server_converter_url().geturl(), json=payload, headers=headers, verify = config.ssl_verify_peer_mode_enabled(), timeout=5) # send the headers and body values to the converter and write the result to the response
|
||||
status_code = response.status_code
|
||||
if status_code != 200: # checking status code
|
||||
raise RuntimeError('Convertation service returned status: %s' % status_code)
|
||||
|
||||
@ -16,11 +16,16 @@
|
||||
|
||||
"""
|
||||
|
||||
import shutil
|
||||
|
||||
import config
|
||||
from copy import deepcopy
|
||||
import requests
|
||||
import os
|
||||
import json
|
||||
from src.configuration import ConfigurationManager
|
||||
from src.history import HistoryManager, HistoryChanges
|
||||
from src.storage import StorageManager
|
||||
from src.proxy import ProxyManager
|
||||
from . import jwtManager, docManager, historyManager, fileUtils, serviceConverter
|
||||
|
||||
# read request body
|
||||
@ -30,8 +35,8 @@ def readBody(request):
|
||||
token = body.get('token') # get the document token
|
||||
|
||||
if (not token): # if JSON web token is not received
|
||||
jwtHeader = 'Authorization' if config.DOC_SERV_JWT_HEADER is None or config.DOC_SERV_JWT_HEADER == '' else config.DOC_SERV_JWT_HEADER
|
||||
token = request.headers.get(jwtHeader) # get it from the Authorization header
|
||||
config = ConfigurationManager()
|
||||
token = request.headers.get(config.jwt_header()) # get it from the Authorization header
|
||||
if token:
|
||||
token = token[len('Bearer '):] # and save it without Authorization prefix
|
||||
|
||||
@ -44,7 +49,9 @@ def readBody(request):
|
||||
return body
|
||||
|
||||
# file saving process
|
||||
def processSave(body, filename, usAddr):
|
||||
def processSave(raw_body, filename, usAddr):
|
||||
body = resolve_process_save_body(raw_body)
|
||||
|
||||
download = body.get('url')
|
||||
if (download is None):
|
||||
raise Exception("DownloadUrl is null")
|
||||
@ -66,35 +73,33 @@ def processSave(body, filename, usAddr):
|
||||
except Exception:
|
||||
newFilename = docManager.getCorrectName(fileUtils.getFileNameWithoutExt(filename) + downloadExt, usAddr)
|
||||
|
||||
path = docManager.getStoragePath(newFilename, usAddr) # get the file path
|
||||
|
||||
data = docManager.downloadFileFromUri(download) # download document file
|
||||
data = docManager.downloadFileFromUri(download)
|
||||
if (data is None):
|
||||
raise Exception("Downloaded document is null")
|
||||
|
||||
histDir = historyManager.getHistoryDir(path) # get the path to the history direction
|
||||
if not os.path.exists(histDir): # if the path doesn't exist
|
||||
os.makedirs(histDir) # create it
|
||||
|
||||
versionDir = historyManager.getNextVersionDir(histDir) # get the path to the next file version
|
||||
|
||||
os.rename(docManager.getStoragePath(filename, usAddr), historyManager.getPrevFilePath(versionDir, curExt)) # get the path to the previous file version and rename the storage path with it
|
||||
|
||||
docManager.saveFile(data, path) # save document file
|
||||
|
||||
dataChanges = docManager.downloadFileFromUri(changesUri) # download changes file
|
||||
dataChanges = docManager.downloadFileFromUri(changesUri)
|
||||
if (dataChanges is None):
|
||||
raise Exception("Downloaded changes is null")
|
||||
docManager.saveFile(dataChanges, historyManager.getChangesZipPath(versionDir)) # save file changes to the diff.zip archive
|
||||
|
||||
hist = None
|
||||
hist = body.get('changeshistory')
|
||||
if (not hist) & ('history' in body):
|
||||
hist = json.dumps(body.get('history'))
|
||||
if hist:
|
||||
historyManager.writeFile(historyManager.getChangesHistoryPath(versionDir), hist) # write the history changes to the changes.json file
|
||||
|
||||
historyManager.writeFile(historyManager.getKeyPath(versionDir), body.get('key')) # write the key value to the key.txt file
|
||||
config_manager = ConfigurationManager()
|
||||
storage_manager = StorageManager(
|
||||
config_manager=config_manager,
|
||||
user_host=usAddr,
|
||||
source_basename=newFilename
|
||||
)
|
||||
history_manager = HistoryManager(
|
||||
storage_manager=storage_manager
|
||||
)
|
||||
history_changes = HistoryChanges.decode(hist)
|
||||
history_manager.save(
|
||||
changes=history_changes,
|
||||
diff=dataChanges.iter_content(chunk_size=8192),
|
||||
item=data.iter_content(chunk_size=8192)
|
||||
)
|
||||
|
||||
forcesavePath = docManager.getForcesavePath(newFilename, usAddr, False) # get the path to the forcesaved file version
|
||||
if (forcesavePath != ""): # if the forcesaved file version exists
|
||||
@ -152,7 +157,7 @@ def processForceSave(body, filename, usAddr):
|
||||
|
||||
# create a command request
|
||||
def commandRequest(method, key, meta = None):
|
||||
documentCommandUrl = config.DOC_SERV_SITE_URL + config.DOC_SERV_COMMAND_URL
|
||||
config = ConfigurationManager()
|
||||
|
||||
payload = {
|
||||
'c': method,
|
||||
@ -166,15 +171,45 @@ def commandRequest(method, key, meta = None):
|
||||
headers={'accept': 'application/json'}
|
||||
|
||||
if (jwtManager.isEnabled() and jwtManager.useForRequest()): # check if a secret key to generate token exists or not
|
||||
jwtHeader = 'Authorization' if config.DOC_SERV_JWT_HEADER is None or config.DOC_SERV_JWT_HEADER == '' else config.DOC_SERV_JWT_HEADER # get jwt header
|
||||
config = ConfigurationManager()
|
||||
headerToken = jwtManager.encode({'payload': payload}) # encode a payload object into a header token
|
||||
headers[jwtHeader] = f'Bearer {headerToken}' # add a header Authorization with a header token with Authorization prefix in it
|
||||
headers[config.jwt_header()] = f'Bearer {headerToken}' # add a header Authorization with a header token with Authorization prefix in it
|
||||
|
||||
payload['token'] = jwtManager.encode(payload) # encode a payload object into a body token
|
||||
response = requests.post(documentCommandUrl, json=payload, headers=headers, verify = config.DOC_SERV_VERIFY_PEER)
|
||||
response = requests.post(config.document_server_command_url().geturl(), json=payload, headers=headers, verify = config.ssl_verify_peer_mode_enabled())
|
||||
|
||||
if (meta):
|
||||
return response
|
||||
|
||||
return
|
||||
|
||||
def resolve_process_save_body(body):
|
||||
copied = deepcopy(body)
|
||||
config_manager = ConfigurationManager()
|
||||
proxy_manager = ProxyManager(config_manager=config_manager)
|
||||
|
||||
url = copied.get('url')
|
||||
if url is not None:
|
||||
resolved_url = proxy_manager.resolve_document_server_url(url)
|
||||
copied['url'] = resolved_url.geturl()
|
||||
|
||||
changes_url = copied.get('changesurl')
|
||||
if changes_url is not None:
|
||||
resolved_url = proxy_manager.resolve_document_server_url(changes_url)
|
||||
copied['changesurl'] = resolved_url.geturl()
|
||||
|
||||
home = copied.get('home')
|
||||
if home is not None:
|
||||
url = home.get('url')
|
||||
if url is not None:
|
||||
resolved_url = proxy_manager.resolve_document_server_url(url)
|
||||
home['url'] = resolved_url.geturl()
|
||||
|
||||
changes_url = home.get('changesurl')
|
||||
if changes_url is not None:
|
||||
resolved_url = proxy_manager.resolve_document_server_url(changes_url)
|
||||
home['changesurl'] = resolved_url.geturl()
|
||||
|
||||
copied['home'] = home
|
||||
|
||||
return copied
|
||||
|
||||
@ -111,12 +111,7 @@ def getAllUsers():
|
||||
# get user information from the request
|
||||
def getUserFromReq(req):
|
||||
uid = req.COOKIES.get('uid')
|
||||
|
||||
for user in USERS:
|
||||
if (user.id == uid):
|
||||
return user
|
||||
|
||||
return DEFAULT_USER
|
||||
return find_user(uid)
|
||||
|
||||
# get users data for mentions
|
||||
def getUsersForMentions(uid):
|
||||
@ -125,3 +120,10 @@ def getUsersForMentions(uid):
|
||||
if(user.id != uid and user.name != None and user.email != None):
|
||||
usersData.append({'name':user.name, 'email':user.email})
|
||||
return usersData
|
||||
|
||||
def find_user(id: str) -> User:
|
||||
for user in USERS:
|
||||
if not user.id == id:
|
||||
continue
|
||||
return user
|
||||
return DEFAULT_USER
|
||||
|
||||
@ -17,24 +17,28 @@
|
||||
"""
|
||||
import requests
|
||||
|
||||
import config
|
||||
import json
|
||||
import os
|
||||
import urllib.parse
|
||||
from pathlib import Path
|
||||
|
||||
from datetime import datetime
|
||||
from django.http import HttpResponse, HttpResponseRedirect, FileResponse
|
||||
from django.shortcuts import render
|
||||
from src.configuration import ConfigurationManager
|
||||
from src.history import HistoryManager
|
||||
from src.request import RequestManager
|
||||
from src.storage import StorageManager
|
||||
from src.utils import docManager, fileUtils, serviceConverter, users, jwtManager, historyManager, trackManager
|
||||
|
||||
|
||||
# upload a file from the document storage service to the document editing service
|
||||
def upload(request):
|
||||
response = {}
|
||||
|
||||
try:
|
||||
fileInfo = request.FILES['uploadedFile']
|
||||
if ((fileInfo.size > config.FILE_SIZE_MAX) | (fileInfo.size <= 0)): # check if the file size exceeds the maximum size allowed (5242880)
|
||||
config = ConfigurationManager()
|
||||
if ((fileInfo.size > config.maximum_file_size()) | (fileInfo.size <= 0)): # check if the file size exceeds the maximum size allowed (5242880)
|
||||
raise Exception('File size is incorrect')
|
||||
|
||||
curExt = fileUtils.getFileExt(fileInfo.name)
|
||||
@ -115,11 +119,13 @@ def saveAs(request):
|
||||
saveAsFileUrl = body.get('url')
|
||||
title = body.get('title')
|
||||
|
||||
config = ConfigurationManager()
|
||||
|
||||
filename = docManager.getCorrectName(title, request)
|
||||
path = docManager.getStoragePath(filename, request)
|
||||
resp = requests.get(saveAsFileUrl, verify = config.DOC_SERV_VERIFY_PEER)
|
||||
resp = requests.get(saveAsFileUrl, verify = config.ssl_verify_peer_mode_enabled())
|
||||
|
||||
if ((len(resp.content) > config.FILE_SIZE_MAX) | (len(resp.content) <= 0)): # check if the file size exceeds the maximum size allowed (5242880)
|
||||
if ((len(resp.content) > config.maximum_file_size()) | (len(resp.content) <= 0)): # check if the file size exceeds the maximum size allowed (5242880)
|
||||
response.setdefault('error', 'File size is incorrect')
|
||||
raise Exception('File size is incorrect')
|
||||
|
||||
@ -161,14 +167,31 @@ def rename(request):
|
||||
# edit a file
|
||||
def edit(request):
|
||||
filename = fileUtils.getFileName(request.GET['filename'])
|
||||
|
||||
config_manager = ConfigurationManager()
|
||||
request_manager = RequestManager(
|
||||
request=request
|
||||
)
|
||||
user_host = request_manager.resolve_address()
|
||||
storage_manager = StorageManager(
|
||||
config_manager=config_manager,
|
||||
user_host=user_host,
|
||||
source_basename=filename
|
||||
)
|
||||
history_manager = HistoryManager(
|
||||
storage_manager=storage_manager
|
||||
)
|
||||
latest_version = history_manager.latest_version()
|
||||
key = history_manager.key(latest_version)
|
||||
|
||||
isEnableDirectUrl = request.GET['directUrl'].lower() in ("true") if 'directUrl' in request.GET else False
|
||||
|
||||
ext = fileUtils.getFileExt(filename)
|
||||
|
||||
fileUri = docManager.getFileUri(filename, True, request)
|
||||
fileUriUser = docManager.getDownloadUrl(filename, request) + "&dmode=emb" if os.path.isabs(config.STORAGE_PATH) else docManager.getFileUri(filename, False, request)
|
||||
fileUriUser = docManager.getDownloadUrl(filename, request) + "&dmode=emb"
|
||||
directUrl = docManager.getDownloadUrl(filename, request, False)
|
||||
docKey = docManager.generateFileKey(filename, request)
|
||||
docKey = key
|
||||
fileType = fileUtils.getFileType(filename)
|
||||
user = users.getUserFromReq(request) # get user
|
||||
|
||||
@ -256,9 +279,9 @@ def edit(request):
|
||||
'lang': lang,
|
||||
'callbackUrl': docManager.getCallbackUrl(filename, request), # absolute URL to the document storage service
|
||||
'coEditing': {
|
||||
"mode": "strict",
|
||||
"mode": "strict",
|
||||
"change": False
|
||||
}
|
||||
}
|
||||
if edMode == 'view' and user.id =='uid-0' else None,
|
||||
'createUrl' : createUrl if user.id !='uid-0' else None,
|
||||
'templates' : templates if user.templates else None,
|
||||
@ -275,11 +298,11 @@ def edit(request):
|
||||
},
|
||||
'customization': { # the parameters for the editor interface
|
||||
'about': True, # the About section display
|
||||
'comments': True,
|
||||
'comments': True,
|
||||
'feedback': True, # the Feedback & Support menu button display
|
||||
'forcesave': False, # adds the request for the forced file saving to the callback handler
|
||||
'submitForm': submitForm, # if the Submit form button is displayed or not
|
||||
'goback': { # settings for the Open file location menu button and upper right corner button
|
||||
'goback': { # settings for the Open file location menu button and upper right corner button
|
||||
'url': docManager.getServerUrl(False, request) # the absolute URL to the website address which will be opened when clicking the Open file location menu button
|
||||
}
|
||||
}
|
||||
@ -317,7 +340,7 @@ def edit(request):
|
||||
}
|
||||
|
||||
# users data for mentions
|
||||
usersForMentions = users.getUsersForMentions(user.id)
|
||||
usersForMentions = users.getUsersForMentions(user.id)
|
||||
|
||||
if jwtManager.isEnabled(): # if the secret key to generate token exists
|
||||
edConfig['token'] = jwtManager.encode(edConfig) # encode the edConfig object into a token
|
||||
@ -325,14 +348,16 @@ def edit(request):
|
||||
dataCompareFile['token'] = jwtManager.encode(dataCompareFile) # encode the dataCompareFile object into a token
|
||||
dataMailMergeRecipients['token'] = jwtManager.encode(dataMailMergeRecipients) # encode the dataMailMergeRecipients object into a token
|
||||
|
||||
hist = historyManager.getHistoryObject(storagePath, filename, docKey, fileUri, isEnableDirectUrl, request) # get the document history
|
||||
# hist = historyManager.getHistoryObject(storagePath, filename, docKey, fileUri, isEnableDirectUrl, request) # get the document history
|
||||
|
||||
config = ConfigurationManager()
|
||||
|
||||
context = { # the data that will be passed to the template
|
||||
'cfg': json.dumps(edConfig), # the document config in json format
|
||||
'history': json.dumps(hist['history']) if 'history' in hist else None, # the information about the current version
|
||||
'historyData': json.dumps(hist['historyData']) if 'historyData' in hist else None, # the information about the previous document versions if they exist
|
||||
# 'history': json.dumps(hist['history']) if 'history' in hist else None, # the information about the current version
|
||||
# 'historyData': json.dumps(hist['historyData']) if 'historyData' in hist else None, # the information about the previous document versions if they exist
|
||||
'fileType': fileType, # the file type of the document (text, spreadsheet or presentation)
|
||||
'apiUrl': config.DOC_SERV_SITE_URL + config.DOC_SERV_API_URL, # the absolute URL to the api
|
||||
'apiUrl': config.document_server_api_url().geturl(), # the absolute URL to the api
|
||||
'dataInsertImage': json.dumps(dataInsertImage)[1 : len(json.dumps(dataInsertImage)) - 1], # the image which will be inserted into the document
|
||||
'dataCompareFile': dataCompareFile, # document which will be compared with the current document
|
||||
'dataMailMergeRecipients': json.dumps(dataMailMergeRecipients), # recipient data for mail merging
|
||||
@ -404,8 +429,8 @@ def download(request):
|
||||
isEmbedded = request.GET.get('dmode')
|
||||
|
||||
if (jwtManager.isEnabled() and isEmbedded == None and userAddress and jwtManager.useForRequest()):
|
||||
jwtHeader = 'Authorization' if config.DOC_SERV_JWT_HEADER is None or config.DOC_SERV_JWT_HEADER == '' else config.DOC_SERV_JWT_HEADER
|
||||
token = request.headers.get(jwtHeader)
|
||||
config = ConfigurationManager()
|
||||
token = request.headers.get(config.jwt_header())
|
||||
if token:
|
||||
token = token[len('Bearer '):]
|
||||
|
||||
@ -437,8 +462,8 @@ def downloadhistory(request):
|
||||
isEmbedded = request.GET.get('dmode')
|
||||
|
||||
if (jwtManager.isEnabled() and isEmbedded == None and jwtManager.useForRequest()):
|
||||
jwtHeader = 'Authorization' if config.DOC_SERV_JWT_HEADER is None or config.DOC_SERV_JWT_HEADER == '' else config.DOC_SERV_JWT_HEADER
|
||||
token = request.headers.get(jwtHeader)
|
||||
config = ConfigurationManager()
|
||||
token = request.headers.get(config.jwt_header())
|
||||
if token:
|
||||
token = token[len('Bearer '):]
|
||||
try:
|
||||
@ -477,20 +502,20 @@ def reference(request):
|
||||
userAddress = fileKey['userAddress']
|
||||
if userAddress == request.META['REMOTE_ADDR']:
|
||||
fileName = fileKey['fileName']
|
||||
|
||||
|
||||
if fileName is None:
|
||||
try:
|
||||
path = fileUtils.getFileName(body['path'])
|
||||
if os.path.exists(docManager.getStoragePath(path,request)):
|
||||
fileName = path
|
||||
fileName = path
|
||||
except KeyError:
|
||||
response.setdefault('error', 'Path not found')
|
||||
return HttpResponse(json.dumps(response), content_type='application/json', status=404)
|
||||
|
||||
|
||||
if fileName is None:
|
||||
response.setdefault('error', 'File not found')
|
||||
return HttpResponse(json.dumps(response), content_type='application/json', status=404)
|
||||
|
||||
|
||||
data = {
|
||||
'fileType' : fileUtils.getFileExt(fileName),
|
||||
'url' : docManager.getDownloadUrl(fileName, request),
|
||||
@ -504,5 +529,5 @@ def reference(request):
|
||||
|
||||
if (jwtManager.isEnabled()):
|
||||
data['token'] = jwtManager.encode(data)
|
||||
|
||||
|
||||
return HttpResponse(json.dumps(data), content_type='application/json')
|
||||
|
||||
@ -18,11 +18,11 @@
|
||||
|
||||
import re
|
||||
import sys
|
||||
import config
|
||||
import json
|
||||
|
||||
from django.shortcuts import render
|
||||
|
||||
from src.configuration import ConfigurationManager
|
||||
from src.utils import users
|
||||
from src.utils import docManager
|
||||
|
||||
@ -33,14 +33,15 @@ def getDirectUrlParam(request):
|
||||
return False;
|
||||
|
||||
def default(request): # default parameters that will be passed to the template
|
||||
config = ConfigurationManager()
|
||||
context = {
|
||||
'users': users.USERS,
|
||||
'languages': config.LANGUAGES,
|
||||
'preloadurl': config.DOC_SERV_SITE_URL + config.DOC_SERV_PRELOADER_URL,
|
||||
'editExt': json.dumps(config.DOC_SERV_EDITED), # file extensions that can be edited
|
||||
'convExt': json.dumps(config.DOC_SERV_CONVERT), # file extensions that can be converted
|
||||
'languages': config.languages(),
|
||||
'preloadurl': config.document_server_preloader_url().geturl(),
|
||||
'editExt': json.dumps(config.editable_file_extensions()), # file extensions that can be edited
|
||||
'convExt': json.dumps(config.convertible_file_extensions()), # file extensions that can be converted
|
||||
'files': docManager.getStoredFiles(request), # information about stored files
|
||||
'fillExt': json.dumps(config.DOC_SERV_FILLFORMS),
|
||||
'fillExt': json.dumps(config.fillable_file_extensions()),
|
||||
'directUrl': str(getDirectUrlParam(request)).lower
|
||||
}
|
||||
return render(request, 'index.html', context) # execute the "index.html" template with context data and return http response in json format
|
||||
@ -1,16 +0,0 @@
|
||||
"""
|
||||
WSGI config for example project.
|
||||
|
||||
It exposes the WSGI callable as a module-level variable named ``application``.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/3.1/howto/deployment/wsgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'src.settings')
|
||||
|
||||
application = get_wsgi_application()
|
||||
@ -183,6 +183,68 @@
|
||||
}
|
||||
};
|
||||
|
||||
function onRequestHistory() {
|
||||
const query = new URLSearchParams(window.location.search)
|
||||
const sourceBasename = query.get('filename')
|
||||
const request = new XMLHttpRequest()
|
||||
const path = `history/${sourceBasename}`
|
||||
request.open("GET", path)
|
||||
request.send()
|
||||
request.onload = function () {
|
||||
response = JSON.parse(request.response)
|
||||
if (request.status != 200) {
|
||||
innerAlert(response.error)
|
||||
return
|
||||
}
|
||||
docEditor.refreshHistory(response)
|
||||
}
|
||||
}
|
||||
|
||||
function onRequestHistoryData(event) {
|
||||
const query = new URLSearchParams(window.location.search)
|
||||
const sourceBasename = query.get('filename')
|
||||
const version = event.data
|
||||
const direct = query.has('directURL')
|
||||
const request = new XMLHttpRequest()
|
||||
const path = direct
|
||||
? `history/${sourceBasename}/${version}/data?direct`
|
||||
: `history/${sourceBasename}/${version}/data`
|
||||
request.open("GET", path)
|
||||
request.send()
|
||||
request.onload = function () {
|
||||
response = JSON.parse(request.response)
|
||||
if (request.status != 200) {
|
||||
innerAlert(response.error)
|
||||
return
|
||||
}
|
||||
docEditor.setHistoryData(response)
|
||||
}
|
||||
}
|
||||
|
||||
function onRequestHistoryClose() {
|
||||
document.location.reload()
|
||||
}
|
||||
|
||||
function onRequestRestore(event) {
|
||||
const query = new URLSearchParams(window.location.search)
|
||||
const sourceBasename = query.get('filename')
|
||||
const config = {{ cfg | safe }}
|
||||
const userID = config.editorConfig.user.id
|
||||
const version = event.data.version
|
||||
const request = new XMLHttpRequest()
|
||||
const path = `history/${sourceBasename}/${version}/restore?userId=${userID}`
|
||||
request.open("PUT", path)
|
||||
request.send()
|
||||
request.onload = function () {
|
||||
if (request.status != 200) {
|
||||
response = JSON.parse(request.response)
|
||||
innerAlert(response.error)
|
||||
return
|
||||
}
|
||||
onRequestHistory()
|
||||
}
|
||||
}
|
||||
|
||||
var connectEditor = function () {
|
||||
|
||||
config = {{ cfg | safe }}
|
||||
@ -198,31 +260,15 @@
|
||||
'onRequestInsertImage': onRequestInsertImage,
|
||||
'onRequestCompareFile': onRequestCompareFile,
|
||||
"onRequestMailMergeRecipients": onRequestMailMergeRecipients,
|
||||
'onRequestHistory': onRequestHistory,
|
||||
'onRequestHistoryData': onRequestHistoryData,
|
||||
'onRequestHistoryClose': onRequestHistoryClose,
|
||||
'onRequestRestore': onRequestRestore
|
||||
};
|
||||
|
||||
|
||||
|
||||
if (config.editorConfig.user.id) {
|
||||
|
||||
{% if history and historyData %}
|
||||
|
||||
// the user is trying to show the document version history
|
||||
config.events['onRequestHistory'] = function () {
|
||||
docEditor.refreshHistory({{ history | safe }}); // show the document version history
|
||||
};
|
||||
// the user is trying to click the specific document version in the document version history
|
||||
config.events['onRequestHistoryData'] = function (event) {
|
||||
var ver = event.data;
|
||||
var histData = {{ historyData | safe }};
|
||||
docEditor.setHistoryData(histData[ver - 1]); // send the link to the document for viewing the version history
|
||||
};
|
||||
// the user is trying to go back to the document from viewing the document version history
|
||||
config.events['onRequestHistoryClose'] = function () {
|
||||
document.location.reload();
|
||||
};
|
||||
|
||||
{% endif %}
|
||||
|
||||
// add mentions for not anonymous users
|
||||
config.events['onRequestUsers'] = function () {
|
||||
docEditor.setUsers({ // set a list of users to mention in the comments
|
||||
|
||||
Reference in New Issue
Block a user