├── .gitignore ├── .travis.yml ├── CHANGES.rst ├── CONTRIBUTORS.rst ├── LICENSE.txt ├── MANIFEST.in ├── README.rst ├── docs ├── Makefile ├── make.bat └── source │ ├── CHANGES.rst │ ├── CONTRIBUTORS.rst │ ├── README.rst │ ├── conf.py │ ├── index.rst │ ├── modules.rst │ ├── owncloud.owncloud.rst │ └── owncloud.rst ├── owncloud ├── __init__.py ├── owncloud.py └── test │ ├── __init__.py │ ├── config.py.sample │ └── test.py ├── runtests.sh ├── setup.cfg ├── setup.py └── travis-build └── setup_oc_docker.sh /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.pyo 3 | **/build/* 4 | build/ 5 | dist/ 6 | *.egg-info/ 7 | MANIFEST 8 | .idea/ 9 | owncloud/test/config.py 10 | .venv 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.7" 4 | - "3.8" 5 | services: 6 | - docker 7 | env: 8 | - OC_VERSION=10.0 9 | 10 | before_install: 11 | - travis-build/setup_oc_docker.sh $OC_VERSION 12 | 13 | install: 14 | - pip install flake8 requests unittest-data-provider six 15 | 16 | script: 17 | - flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics 18 | - cp owncloud/test/config.py.sample owncloud/test/config.py 19 | # execute unit tests 20 | - ./runtests.sh 21 | 22 | -------------------------------------------------------------------------------- /CHANGES.rst: -------------------------------------------------------------------------------- 1 | docs/source/CHANGES.rst -------------------------------------------------------------------------------- /CONTRIBUTORS.rst: -------------------------------------------------------------------------------- 1 | docs/source/CONTRIBUTORS.rst -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Vincent Petry 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.txt *.rst 2 | recursive-include docs/source *.rst 3 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | docs/source/README.rst -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source 21 | 22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 23 | 24 | help: 25 | @echo "Please use \`make ' where is one of" 26 | @echo " html to make standalone HTML files" 27 | @echo " dirhtml to make HTML files named index.html in directories" 28 | @echo " singlehtml to make a single large HTML file" 29 | @echo " pickle to make pickle files" 30 | @echo " json to make JSON files" 31 | @echo " htmlhelp to make HTML files and a HTML help project" 32 | @echo " qthelp to make HTML files and a qthelp project" 33 | @echo " devhelp to make HTML files and a Devhelp project" 34 | @echo " epub to make an epub" 35 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 36 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 37 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 38 | @echo " text to make text files" 39 | @echo " man to make manual pages" 40 | @echo " texinfo to make Texinfo files" 41 | @echo " info to make Texinfo files and run them through makeinfo" 42 | @echo " gettext to make PO message catalogs" 43 | @echo " changes to make an overview of all changed/added/deprecated items" 44 | @echo " xml to make Docutils-native XML files" 45 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 46 | @echo " linkcheck to check all external links for integrity" 47 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 48 | 49 | clean: 50 | rm -rf $(BUILDDIR)/* 51 | 52 | html: 53 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 54 | @echo 55 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 56 | 57 | dirhtml: 58 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 59 | @echo 60 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 61 | 62 | singlehtml: 63 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 64 | @echo 65 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 66 | 67 | pickle: 68 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 69 | @echo 70 | @echo "Build finished; now you can process the pickle files." 71 | 72 | json: 73 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 74 | @echo 75 | @echo "Build finished; now you can process the JSON files." 76 | 77 | htmlhelp: 78 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 79 | @echo 80 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 81 | ".hhp project file in $(BUILDDIR)/htmlhelp." 82 | 83 | qthelp: 84 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 85 | @echo 86 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 87 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 88 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/ownCloudClient.qhcp" 89 | @echo "To view the help file:" 90 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/ownCloudClient.qhc" 91 | 92 | devhelp: 93 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 94 | @echo 95 | @echo "Build finished." 96 | @echo "To view the help file:" 97 | @echo "# mkdir -p $$HOME/.local/share/devhelp/ownCloudClient" 98 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/ownCloudClient" 99 | @echo "# devhelp" 100 | 101 | epub: 102 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 103 | @echo 104 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 105 | 106 | latex: 107 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 108 | @echo 109 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 110 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 111 | "(use \`make latexpdf' here to do that automatically)." 112 | 113 | latexpdf: 114 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 115 | @echo "Running LaTeX files through pdflatex..." 116 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 117 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 118 | 119 | latexpdfja: 120 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 121 | @echo "Running LaTeX files through platex and dvipdfmx..." 122 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 123 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 124 | 125 | text: 126 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 127 | @echo 128 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 129 | 130 | man: 131 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 132 | @echo 133 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 134 | 135 | texinfo: 136 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 137 | @echo 138 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 139 | @echo "Run \`make' in that directory to run these through makeinfo" \ 140 | "(use \`make info' here to do that automatically)." 141 | 142 | info: 143 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 144 | @echo "Running Texinfo files through makeinfo..." 145 | make -C $(BUILDDIR)/texinfo info 146 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 147 | 148 | gettext: 149 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 150 | @echo 151 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 152 | 153 | changes: 154 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 155 | @echo 156 | @echo "The overview file is in $(BUILDDIR)/changes." 157 | 158 | linkcheck: 159 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 160 | @echo 161 | @echo "Link check complete; look for any errors in the above output " \ 162 | "or in $(BUILDDIR)/linkcheck/output.txt." 163 | 164 | doctest: 165 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 166 | @echo "Testing of doctests in the sources finished, look at the " \ 167 | "results in $(BUILDDIR)/doctest/output.txt." 168 | 169 | xml: 170 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 171 | @echo 172 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 173 | 174 | pseudoxml: 175 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 176 | @echo 177 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 178 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% source 10 | set I18NSPHINXOPTS=%SPHINXOPTS% source 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 31 | echo. text to make text files 32 | echo. man to make manual pages 33 | echo. texinfo to make Texinfo files 34 | echo. gettext to make PO message catalogs 35 | echo. changes to make an overview over all changed/added/deprecated items 36 | echo. xml to make Docutils-native XML files 37 | echo. pseudoxml to make pseudoxml-XML files for display purposes 38 | echo. linkcheck to check all external links for integrity 39 | echo. doctest to run all doctests embedded in the documentation if enabled 40 | goto end 41 | ) 42 | 43 | if "%1" == "clean" ( 44 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 45 | del /q /s %BUILDDIR%\* 46 | goto end 47 | ) 48 | 49 | 50 | %SPHINXBUILD% 2> nul 51 | if errorlevel 9009 ( 52 | echo. 53 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 54 | echo.installed, then set the SPHINXBUILD environment variable to point 55 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 56 | echo.may add the Sphinx directory to PATH. 57 | echo. 58 | echo.If you don't have Sphinx installed, grab it from 59 | echo.http://sphinx-doc.org/ 60 | exit /b 1 61 | ) 62 | 63 | if "%1" == "html" ( 64 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 65 | if errorlevel 1 exit /b 1 66 | echo. 67 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 68 | goto end 69 | ) 70 | 71 | if "%1" == "dirhtml" ( 72 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 73 | if errorlevel 1 exit /b 1 74 | echo. 75 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 76 | goto end 77 | ) 78 | 79 | if "%1" == "singlehtml" ( 80 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 81 | if errorlevel 1 exit /b 1 82 | echo. 83 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 84 | goto end 85 | ) 86 | 87 | if "%1" == "pickle" ( 88 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 89 | if errorlevel 1 exit /b 1 90 | echo. 91 | echo.Build finished; now you can process the pickle files. 92 | goto end 93 | ) 94 | 95 | if "%1" == "json" ( 96 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 97 | if errorlevel 1 exit /b 1 98 | echo. 99 | echo.Build finished; now you can process the JSON files. 100 | goto end 101 | ) 102 | 103 | if "%1" == "htmlhelp" ( 104 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 105 | if errorlevel 1 exit /b 1 106 | echo. 107 | echo.Build finished; now you can run HTML Help Workshop with the ^ 108 | .hhp project file in %BUILDDIR%/htmlhelp. 109 | goto end 110 | ) 111 | 112 | if "%1" == "qthelp" ( 113 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 114 | if errorlevel 1 exit /b 1 115 | echo. 116 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 117 | .qhcp project file in %BUILDDIR%/qthelp, like this: 118 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\ownCloudClient.qhcp 119 | echo.To view the help file: 120 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\ownCloudClient.ghc 121 | goto end 122 | ) 123 | 124 | if "%1" == "devhelp" ( 125 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 126 | if errorlevel 1 exit /b 1 127 | echo. 128 | echo.Build finished. 129 | goto end 130 | ) 131 | 132 | if "%1" == "epub" ( 133 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 134 | if errorlevel 1 exit /b 1 135 | echo. 136 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 137 | goto end 138 | ) 139 | 140 | if "%1" == "latex" ( 141 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 142 | if errorlevel 1 exit /b 1 143 | echo. 144 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 145 | goto end 146 | ) 147 | 148 | if "%1" == "latexpdf" ( 149 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 150 | cd %BUILDDIR%/latex 151 | make all-pdf 152 | cd %BUILDDIR%/.. 153 | echo. 154 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 155 | goto end 156 | ) 157 | 158 | if "%1" == "latexpdfja" ( 159 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 160 | cd %BUILDDIR%/latex 161 | make all-pdf-ja 162 | cd %BUILDDIR%/.. 163 | echo. 164 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 165 | goto end 166 | ) 167 | 168 | if "%1" == "text" ( 169 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 170 | if errorlevel 1 exit /b 1 171 | echo. 172 | echo.Build finished. The text files are in %BUILDDIR%/text. 173 | goto end 174 | ) 175 | 176 | if "%1" == "man" ( 177 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 178 | if errorlevel 1 exit /b 1 179 | echo. 180 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 181 | goto end 182 | ) 183 | 184 | if "%1" == "texinfo" ( 185 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 186 | if errorlevel 1 exit /b 1 187 | echo. 188 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 189 | goto end 190 | ) 191 | 192 | if "%1" == "gettext" ( 193 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 194 | if errorlevel 1 exit /b 1 195 | echo. 196 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 197 | goto end 198 | ) 199 | 200 | if "%1" == "changes" ( 201 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 202 | if errorlevel 1 exit /b 1 203 | echo. 204 | echo.The overview file is in %BUILDDIR%/changes. 205 | goto end 206 | ) 207 | 208 | if "%1" == "linkcheck" ( 209 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 210 | if errorlevel 1 exit /b 1 211 | echo. 212 | echo.Link check complete; look for any errors in the above output ^ 213 | or in %BUILDDIR%/linkcheck/output.txt. 214 | goto end 215 | ) 216 | 217 | if "%1" == "doctest" ( 218 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 219 | if errorlevel 1 exit /b 1 220 | echo. 221 | echo.Testing of doctests in the sources finished, look at the ^ 222 | results in %BUILDDIR%/doctest/output.txt. 223 | goto end 224 | ) 225 | 226 | if "%1" == "xml" ( 227 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml 228 | if errorlevel 1 exit /b 1 229 | echo. 230 | echo.Build finished. The XML files are in %BUILDDIR%/xml. 231 | goto end 232 | ) 233 | 234 | if "%1" == "pseudoxml" ( 235 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml 236 | if errorlevel 1 exit /b 1 237 | echo. 238 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. 239 | goto end 240 | ) 241 | 242 | :end 243 | -------------------------------------------------------------------------------- /docs/source/CHANGES.rst: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========= 3 | 4 | 0.6 5 | --- 6 | 7 | - Added support to query arbitrary properties with file info and file listing [NikosChondros] 8 | - Added support for file operations within a public link [mrwunderbar666] 9 | 10 | 0.5 11 | --- 12 | 13 | - Added "name" attribute for public links [PVince81] 14 | - Fixed deprecation warnings [Tilman Lüttje] [PVince81] 15 | - Added support sharing with federated users [remjg] 16 | - Fixed setup script for utf-8 paths [amicitas] 17 | - Fixed file mtime parsing issue [viraj96] 18 | - Add support for the server's DAV v2 endpoint [PVince81] 19 | - Remove support for ownCloud 8.1, 9.0 and 9.1 which are EOL [PVince81] 20 | 21 | 0.4 22 | --- 23 | 24 | - Some code cleanup removing needless if statements [jamescooke] 25 | - Remove old session_mode [PVince81] 26 | - Set Depth to 0 in file_info call [PVince81] 27 | - Make subclassing of Client event easier with protected methods [bobatsar] 28 | 29 | 0.3 30 | --- 31 | 32 | - Make subclassing of Client easier [bobatsar] 33 | - Add Depth param for recursive listing [bobatsar] 34 | - Add shared_with_me parameter to get_shares [bobatsar] 35 | - Link variable is now called url inside of shareinfo [SergioBertolinSG] 36 | - Python3 support [ethifus] [Blizzz] 37 | 38 | 0.2 39 | --- 40 | 41 | - Webdav COPY support [individual-it] 42 | - Added API for federated sharing [nickvergessen] 43 | - Fix login issue in case of failed login [individual-it] 44 | - Added function to get capabilities [SergioBertolinSG] 45 | - Added subadmin APIs for provisioning API [svigneux] 46 | - Tests for provisioning API [individual-it] 47 | - Added provisioning API functions [jennifer] 48 | - Code cleanup / PEP8 formatting [jennifer] 49 | - Added status check function [soalhn] 50 | - Added share API functions [soalhn] [SergioBertolinSG] 51 | - Travis integration [Gomez] 52 | - Added session handling workaround for OC 5 [PVince81] 53 | - Fixed many issues related to unicode path names [PVince81] 54 | - Client now works properly on Windows [PVince81] 55 | 56 | 0.1 57 | --- 58 | 59 | - Make python egg [PVince81] 60 | - Initial release [PVince81] 61 | -------------------------------------------------------------------------------- /docs/source/CONTRIBUTORS.rst: -------------------------------------------------------------------------------- 1 | Contributors 2 | ============ 3 | 4 | * Vincent Petry 5 | * Steffen Lindner 6 | * Soal 7 | * Bruno Santeramo 8 | * jennifer 9 | * Sergio Bertolín 10 | * Alessandro Cosentino 11 | * Mike 12 | * Joas Schilling 13 | * Individual IT Services 14 | * Juan Pablo Villafáñez 15 | * Simon Vigneux 16 | * Roeland Jago Douma 17 | * Erik Pellikka 18 | * Florian Zierer 19 | * mrwunderbar666 20 | * NikosChondros 21 | -------------------------------------------------------------------------------- /docs/source/README.rst: -------------------------------------------------------------------------------- 1 | ================================== 2 | Python client library for ownCloud 3 | ================================== 4 | 5 | .. image:: https://travis-ci.org/owncloud/pyocclient.svg?branch=master 6 | :target: https://travis-ci.org/owncloud/pyocclient 7 | 8 | This pure python library makes it possible to connect to an ownCloud instance 9 | and perform file, share and attribute operations. 10 | 11 | Please note that this is **not** a sync client implementation but a library 12 | that provides functions to abstract away HTTP calls for various ownCloud APIs. 13 | 14 | See the `ownCloud homepage `_ for more information about ownCloud. 15 | 16 | Features 17 | ======== 18 | 19 | Supports connecting to ownCloud 8.2, 9.0, 9.1 and newer. 20 | 21 | General information 22 | ------------------- 23 | 24 | - retrieve information about ownCloud instance (e.g. version, host, URL, etc.) 25 | 26 | Accessing files 27 | --------------- 28 | 29 | - basic file operations like getting a directory listing, file upload/download, directory creation, etc 30 | - read/write file contents from strings 31 | - upload with chunking and mtime keeping 32 | - upload whole directories 33 | - directory download as zip 34 | - access files from public links 35 | - upload files to files drop link target 36 | 37 | Sharing (OCS Share API) 38 | ----------------------- 39 | 40 | - share a file/directory via public link 41 | - share a file/directory with another user or group 42 | - unshare a file/directory 43 | - check if a file/directory is already shared 44 | - get information about a shared resource 45 | - update properties of a known share 46 | 47 | Apps (OCS Provisioning API) 48 | --------------------------- 49 | 50 | - enable/disable apps 51 | - retrieve list of enabled apps 52 | 53 | Users (OCS Provisioning API) 54 | ---------------------------- 55 | 56 | - create/delete users 57 | - create/delete groups 58 | - add/remove user from groups 59 | 60 | App data 61 | -------- 62 | 63 | - store app data as key/values using the privatedata OCS API 64 | 65 | Requirements 66 | ============ 67 | 68 | - Python >= 2.7 or Python >= 3.5 69 | - requests module (for making HTTP requests) 70 | 71 | Installation 72 | ============ 73 | 74 | Automatic installation with pip: 75 | 76 | .. code-block:: bash 77 | 78 | $ pip install pyocclient 79 | 80 | Manual installation of development version with git: 81 | 82 | .. code-block:: bash 83 | 84 | $ pip install requests 85 | $ git clone https://github.com/owncloud/pyocclient.git 86 | $ cd pyocclient 87 | $ python setup.py install 88 | 89 | Usage 90 | ===== 91 | 92 | Example for uploading a file then sharing with link: 93 | 94 | .. code-block:: python 95 | 96 | import owncloud 97 | 98 | oc = owncloud.Client('http://domain.tld/owncloud') 99 | 100 | oc.login('user', 'password') 101 | 102 | oc.mkdir('testdir') 103 | 104 | oc.put_file('testdir/remotefile.txt', 'localfile.txt') 105 | 106 | link_info = oc.share_file_with_link('testdir/remotefile.txt') 107 | 108 | print "Here is your link: " + link_info.get_link() 109 | 110 | Example for uploading a file to a public shared folder: 111 | 112 | .. code-block:: python 113 | 114 | import owncloud 115 | 116 | public_link = 'http://domain.tld/owncloud/A1B2C3D4' 117 | 118 | oc = owncloud.Client.from_public_link(public_link) 119 | oc.drop_file('myfile.zip') 120 | 121 | 122 | Example for downloading a file from a public shared folder with password: 123 | 124 | .. code-block:: python 125 | 126 | import owncloud 127 | 128 | public_link = 'http://domain.tld/owncloud/A1B2C3D4' 129 | folder_password = 'secret' 130 | 131 | oc = owncloud.Client.from_public_link(public_link, folder_password=folder_password) 132 | oc.get_file('/sharedfile.zip', 'download/destination/sharedfile.zip') 133 | 134 | Running the unit tests 135 | ====================== 136 | 137 | To run the unit tests, create a config file called "owncloud/test/config.py". 138 | There is a config file example called "owncloud/test/config.py.sample". All the 139 | information required is in that file. 140 | It should point to a running ownCloud instance to test against. 141 | 142 | You might also need to install the unittest-data-provider package: 143 | 144 | .. code-block:: bash 145 | 146 | $ pip install unittest-data-provider 147 | 148 | Then run the script "runtests.sh": 149 | 150 | .. code-block:: bash 151 | 152 | $ ./runtests.sh 153 | 154 | Building the documentation 155 | ========================== 156 | 157 | To build the documentation, you will need to install Sphinx and docutil. 158 | Then run the following commands: 159 | 160 | .. code-block:: bash 161 | 162 | $ sphinx-apidoc -e -f -o docs/source owncloud/ owncloud/test 163 | $ cd docs 164 | $ make html 165 | 166 | You can then find the documentation inside of "doc/build/html". 167 | 168 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Python ownCloud Client documentation build configuration file, created by 4 | # sphinx-quickstart on Sat Apr 26 17:36:06 2014. 5 | # 6 | # This file is execfile()d with the current directory set to its 7 | # containing dir. 8 | # 9 | # Note that not all possible configuration values are present in this 10 | # autogenerated file. 11 | # 12 | # All configuration values have a default; values that are commented out 13 | # serve to show the default. 14 | 15 | import sys 16 | import os 17 | 18 | # If extensions (or modules to document with autodoc) are in another directory, 19 | # add these directories to sys.path here. If the directory is relative to the 20 | # documentation root, use os.path.abspath to make it absolute, like shown here. 21 | sys.path.insert(0, os.path.abspath('../..')) 22 | 23 | # -- General configuration ------------------------------------------------ 24 | 25 | # If your documentation needs a minimal Sphinx version, state it here. 26 | #needs_sphinx = '1.0' 27 | 28 | # Add any Sphinx extension module names here, as strings. They can be 29 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 30 | # ones. 31 | extensions = [ 32 | 'sphinx.ext.autodoc', 33 | 'sphinx.ext.coverage', 34 | 'sphinx.ext.intersphinx', 35 | ] 36 | 37 | # Add any paths that contain templates here, relative to this directory. 38 | templates_path = ['_templates'] 39 | 40 | # The suffix of source filenames. 41 | source_suffix = '.rst' 42 | 43 | # The encoding of source files. 44 | #source_encoding = 'utf-8-sig' 45 | 46 | # The master toctree document. 47 | master_doc = 'index' 48 | 49 | # General information about the project. 50 | project = u'Python ownCloud Client Library' 51 | copyright = u'2020, Vincent Petry' 52 | 53 | # The version info for the project you're documenting, acts as replacement for 54 | # |version| and |release|, also used in various other places throughout the 55 | # built documents. 56 | # 57 | # The short X.Y version. 58 | version = '0.6' 59 | # The full version, including alpha/beta/rc tags. 60 | release = '0.6' 61 | 62 | # The language for content autogenerated by Sphinx. Refer to documentation 63 | # for a list of supported languages. 64 | #language = None 65 | 66 | # There are two options for replacing |today|: either, you set today to some 67 | # non-false value, then it is used: 68 | #today = '' 69 | # Else, today_fmt is used as the format for a strftime call. 70 | #today_fmt = '%B %d, %Y' 71 | 72 | # List of patterns, relative to source directory, that match files and 73 | # directories to ignore when looking for source files. 74 | exclude_patterns = [] 75 | 76 | # The reST default role (used for this markup: `text`) to use for all 77 | # documents. 78 | #default_role = None 79 | 80 | # If true, '()' will be appended to :func: etc. cross-reference text. 81 | #add_function_parentheses = True 82 | 83 | # If true, the current module name will be prepended to all description 84 | # unit titles (such as .. function::). 85 | #add_module_names = True 86 | 87 | # If true, sectionauthor and moduleauthor directives will be shown in the 88 | # output. They are ignored by default. 89 | #show_authors = False 90 | 91 | # The name of the Pygments (syntax highlighting) style to use. 92 | pygments_style = 'sphinx' 93 | 94 | # A list of ignored prefixes for module index sorting. 95 | #modindex_common_prefix = [] 96 | 97 | # If true, keep warnings as "system message" paragraphs in the built documents. 98 | #keep_warnings = False 99 | 100 | 101 | # -- Options for HTML output ---------------------------------------------- 102 | 103 | # The theme to use for HTML and HTML Help pages. See the documentation for 104 | # a list of builtin themes. 105 | html_theme = 'default' 106 | 107 | # Theme options are theme-specific and customize the look and feel of a theme 108 | # further. For a list of options available for each theme, see the 109 | # documentation. 110 | #html_theme_options = {} 111 | 112 | # Add any paths that contain custom themes here, relative to this directory. 113 | #html_theme_path = [] 114 | 115 | # The name for this set of Sphinx documents. If None, it defaults to 116 | # " v documentation". 117 | #html_title = None 118 | 119 | # A shorter title for the navigation bar. Default is the same as html_title. 120 | #html_short_title = None 121 | 122 | # The name of an image file (relative to this directory) to place at the top 123 | # of the sidebar. 124 | #html_logo = None 125 | 126 | # The name of an image file (within the static path) to use as favicon of the 127 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 128 | # pixels large. 129 | #html_favicon = None 130 | 131 | # Add any paths that contain custom static files (such as style sheets) here, 132 | # relative to this directory. They are copied after the builtin static files, 133 | # so a file named "default.css" will overwrite the builtin "default.css". 134 | html_static_path = ['_static'] 135 | 136 | # Add any extra paths that contain custom files (such as robots.txt or 137 | # .htaccess) here, relative to this directory. These files are copied 138 | # directly to the root of the documentation. 139 | #html_extra_path = [] 140 | 141 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 142 | # using the given strftime format. 143 | #html_last_updated_fmt = '%b %d, %Y' 144 | 145 | # If true, SmartyPants will be used to convert quotes and dashes to 146 | # typographically correct entities. 147 | #html_use_smartypants = True 148 | 149 | # Custom sidebar templates, maps document names to template names. 150 | #html_sidebars = {} 151 | 152 | # Additional templates that should be rendered to pages, maps page names to 153 | # template names. 154 | #html_additional_pages = {} 155 | 156 | # If false, no module index is generated. 157 | #html_domain_indices = True 158 | 159 | # If false, no index is generated. 160 | #html_use_index = True 161 | 162 | # If true, the index is split into individual pages for each letter. 163 | #html_split_index = False 164 | 165 | # If true, links to the reST sources are added to the pages. 166 | #html_show_sourcelink = True 167 | 168 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 169 | #html_show_sphinx = True 170 | 171 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 172 | #html_show_copyright = True 173 | 174 | # If true, an OpenSearch description file will be output, and all pages will 175 | # contain a tag referring to it. The value of this option must be the 176 | # base URL from which the finished HTML is served. 177 | #html_use_opensearch = '' 178 | 179 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 180 | #html_file_suffix = None 181 | 182 | # Output file base name for HTML help builder. 183 | htmlhelp_basename = 'pyocclientdoc' 184 | 185 | 186 | # -- Options for LaTeX output --------------------------------------------- 187 | 188 | latex_elements = { 189 | # The paper size ('letterpaper' or 'a4paper'). 190 | #'papersize': 'letterpaper', 191 | 192 | # The font size ('10pt', '11pt' or '12pt'). 193 | #'pointsize': '10pt', 194 | 195 | # Additional stuff for the LaTeX preamble. 196 | #'preamble': '', 197 | } 198 | 199 | # Grouping the document tree into LaTeX files. List of tuples 200 | # (source start file, target name, title, 201 | # author, documentclass [howto, manual, or own class]). 202 | latex_documents = [ 203 | ('index', 'pyocclient.tex', u'Python ownCloud Client Library Documentation', 204 | u'Vincent Petry', 'manual'), 205 | ] 206 | 207 | # The name of an image file (relative to this directory) to place at the top of 208 | # the title page. 209 | #latex_logo = None 210 | 211 | # For "manual" documents, if this is true, then toplevel headings are parts, 212 | # not chapters. 213 | #latex_use_parts = False 214 | 215 | # If true, show page references after internal links. 216 | #latex_show_pagerefs = False 217 | 218 | # If true, show URL addresses after external links. 219 | #latex_show_urls = False 220 | 221 | # Documents to append as an appendix to all manuals. 222 | #latex_appendices = [] 223 | 224 | # If false, no module index is generated. 225 | #latex_domain_indices = True 226 | 227 | 228 | # -- Options for manual page output --------------------------------------- 229 | 230 | # One entry per manual page. List of tuples 231 | # (source start file, name, description, authors, manual section). 232 | man_pages = [ 233 | ('index', 'owncloud', u'Python ownCloud Client Library Documentation', 234 | [u'Vincent Petry'], 1) 235 | ] 236 | 237 | # If true, show URL addresses after external links. 238 | #man_show_urls = False 239 | 240 | 241 | # -- Options for Texinfo output ------------------------------------------- 242 | 243 | # Grouping the document tree into Texinfo files. List of tuples 244 | # (source start file, target name, title, author, 245 | # dir menu entry, description, category) 246 | texinfo_documents = [ 247 | ('index', 'ownCloudClient', u'Python ownCloud Client Library Documentation', 248 | u'Vincent Petry', 'ownCloudClient', 'Python client library for ownCloud.', 249 | 'Miscellaneous'), 250 | ] 251 | 252 | # Documents to append as an appendix to all manuals. 253 | #texinfo_appendices = [] 254 | 255 | # If false, no module index is generated. 256 | #texinfo_domain_indices = True 257 | 258 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 259 | #texinfo_show_urls = 'footnote' 260 | 261 | # If true, do not generate a @detailmenu in the "Top" node's menu. 262 | #texinfo_no_detailmenu = False 263 | 264 | intersphinx_mapping = { 265 | 'python': ('https://docs.python.org/2.7', None), 266 | } 267 | 268 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | .. ownCloud Client documentation master file, created by 2 | sphinx-quickstart on Sat Apr 26 17:36:06 2014. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | ownCloud client library documentation 7 | ===================================== 8 | 9 | Contents: 10 | 11 | .. toctree:: 12 | :maxdepth: 4 13 | 14 | Description 15 | API Documentation 16 | Contributors 17 | 18 | Indices and tables 19 | ================== 20 | 21 | * :ref:`genindex` 22 | * :ref:`modindex` 23 | * :ref:`search` 24 | 25 | -------------------------------------------------------------------------------- /docs/source/modules.rst: -------------------------------------------------------------------------------- 1 | owncloud 2 | ======== 3 | 4 | .. toctree:: 5 | :maxdepth: 4 6 | 7 | owncloud 8 | -------------------------------------------------------------------------------- /docs/source/owncloud.owncloud.rst: -------------------------------------------------------------------------------- 1 | owncloud.owncloud module 2 | ======================== 3 | 4 | .. automodule:: owncloud.owncloud 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/owncloud.rst: -------------------------------------------------------------------------------- 1 | owncloud package 2 | ================ 3 | 4 | Submodules 5 | ---------- 6 | 7 | .. toctree:: 8 | 9 | owncloud.owncloud 10 | 11 | Module contents 12 | --------------- 13 | 14 | .. automodule:: owncloud 15 | :members: 16 | :undoc-members: 17 | :show-inheritance: 18 | -------------------------------------------------------------------------------- /owncloud/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # vim: expandtab shiftwidth=4 softtabstop=4 4 | # 5 | from .owncloud import * 6 | 7 | -------------------------------------------------------------------------------- /owncloud/owncloud.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # vim: expandtab shiftwidth=4 softtabstop=4 4 | # 5 | """ownCloud client module 6 | 7 | Makes it possible to access files on a remote ownCloud instance, 8 | share them or access application attributes. 9 | """ 10 | 11 | import datetime 12 | import time 13 | import requests 14 | import xml.etree.ElementTree as ET 15 | import os 16 | import math 17 | import six 18 | from six.moves.urllib import parse 19 | 20 | 21 | class ResponseError(Exception): 22 | def __init__(self, res, errorType): 23 | if type(res) is int: 24 | code = res 25 | else: 26 | code = res.status_code 27 | self.res = res 28 | Exception.__init__(self, errorType + " error: %i" % code) 29 | self.status_code = code 30 | 31 | def get_resource_body(self): 32 | if self.res is not None: 33 | return self.res.content 34 | else: 35 | return None 36 | 37 | 38 | class OCSResponseError(ResponseError): 39 | def __init__(self, res): 40 | ResponseError.__init__(self, res, "OCS") 41 | 42 | def get_resource_body(self): 43 | if self.res is not None: 44 | import xml.etree.ElementTree as ElementTree 45 | try: 46 | root_element = ElementTree.fromstringlist(self.res.content) 47 | if root_element.tag == 'message': 48 | return root_element.text 49 | except ET.ParseError: 50 | return self.res.content 51 | else: 52 | return None 53 | 54 | 55 | class HTTPResponseError(ResponseError): 56 | def __init__(self, res): 57 | ResponseError.__init__(self, res, "HTTP") 58 | 59 | 60 | class ShareInfo(object): 61 | """Share information""" 62 | 63 | def __init__(self, share_info): 64 | self.share_info = {} 65 | # remove unneeded attributes 66 | del_attrs = ['item_type', 'item_source', 'file_source', 'parent', 67 | 'storage', 'mail_send'] 68 | for k, v in share_info.items(): 69 | if k not in del_attrs: 70 | self.share_info[k] = v 71 | self.share_id = self._get_int('id') 72 | if 'token' in self.share_info: 73 | self.token = self.share_info['token'] 74 | 75 | def get_id(self): 76 | """Returns the id of the share 77 | 78 | :returns: id of the share 79 | """ 80 | return self._get_int('id') 81 | 82 | def get_share_type(self): 83 | """Returns the type of the share. 84 | See OCS_SHARE_TYPE_* constants. 85 | 86 | :returns: share type 87 | """ 88 | return self._get_int('share_type') 89 | 90 | def get_share_with(self): 91 | """Returns the share recipient. 92 | If share type is OCS_SHARE_TYPE_USER, then the recipient is the name of 93 | the user. 94 | For OCS_SHARE_TYPE_GROUP it is the name of the group. 95 | Otherwise this value is None. 96 | 97 | :returns: name of the share recipient 98 | """ 99 | if 'share_with' in self.share_info: 100 | return self.share_info['share_with'] 101 | return None 102 | 103 | def get_share_with_displayname(self): 104 | """Returns the share recipient displayname. 105 | If share_with_displayname cannot be returned, None is returned instead 106 | :returns: name of the share recipient 107 | """ 108 | if 'share_with_displayname' in self.share_info: 109 | return self.share_info['share_with_displayname'] 110 | return None 111 | 112 | def get_path(self): 113 | """Returns the path of the shared file/folder relative to the 114 | caller's filesystem. 115 | 116 | :returns: path to the shared file/folder 117 | """ 118 | if 'path' in self.share_info: 119 | return self.share_info['path'] 120 | return None 121 | 122 | def get_permissions(self): 123 | """Returns the share permissions. 124 | See OCS_PERMISSION_* constants. 125 | 126 | :returns: share permissions 127 | """ 128 | return self._get_int('permissions') 129 | 130 | def get_share_time(self): 131 | """Returns the share time. 132 | 133 | :returns: share timestamp 134 | :rtype: datetime object 135 | """ 136 | return datetime.datetime.fromtimestamp( 137 | self._get_int('stime') 138 | ) 139 | 140 | def get_expiration(self): 141 | """Returns the expiration date. 142 | 143 | :returns: expiration date 144 | :rtype: datetime object 145 | """ 146 | exp = self._get_int('expiration') 147 | if exp is not None: 148 | return datetime.datetime.fromtimestamp( 149 | exp 150 | ) 151 | return None 152 | 153 | def get_token(self): 154 | if 'token' in self.share_info: 155 | return self.share_info['token'] 156 | return None 157 | 158 | def get_link(self): 159 | if 'url' in self.share_info: 160 | return self.share_info['url'] 161 | return None 162 | 163 | def get_uid_owner(self): 164 | """Returns the user id of the owner. 165 | 166 | :returns: owner user id 167 | """ 168 | if 'uid_owner' in self.share_info: 169 | return self.share_info['uid_owner'] 170 | return None 171 | 172 | def get_displayname_owner(self): 173 | """Returns the display name of the owner. 174 | 175 | :returns: display name of owner 176 | """ 177 | if 'displayname_owner' in self.share_info: 178 | return self.share_info['displayname_owner'] 179 | return None 180 | 181 | def get_name(self): 182 | """Returns the name of a link share 183 | 184 | :returns: name of link share 185 | """ 186 | if 'name' in self.share_info: 187 | return self.share_info['name'] 188 | return None 189 | 190 | def __str__(self): 191 | info = '' 192 | for k, v in self.share_info.items(): 193 | info += '%s=%s,' % (k, v) 194 | return 'ShareInfo(%s)' % info[:-1] 195 | 196 | def __repr__(self): 197 | return self.__str__() 198 | 199 | def _get_int(self, key): 200 | """Simple wrapper which converts value to Integer 201 | in silently manner (w/o raising exception)""" 202 | try: 203 | value = int(self.share_info[key]) 204 | return value 205 | except: 206 | return None 207 | 208 | 209 | class PublicShare(ShareInfo): 210 | """Public share information""" 211 | 212 | def __init__(self, share_id, target_file, link, token): 213 | self.share_id = share_id 214 | self.target_file = target_file 215 | self.link = link 216 | self.token = token 217 | 218 | def __str__(self): 219 | return 'PublicShare(id=%i,path=%s,link=%s,token=%s)' % \ 220 | (self.share_id, self.target_file, self.link, self.token) 221 | 222 | 223 | class FileInfo(object): 224 | """File information""" 225 | 226 | _DATE_FORMAT = '%a, %d %b %Y %H:%M:%S %Z' 227 | 228 | def __init__(self, path, file_type='file', attributes=None): 229 | self.path = path 230 | if path.endswith('/'): 231 | path = path[0:-1] 232 | self.name = os.path.basename(path) 233 | self.file_type = file_type 234 | self.attributes = attributes or {} 235 | 236 | def get_name(self): 237 | """Returns the base name of the file without path 238 | 239 | :returns: name of the file 240 | """ 241 | return self.name 242 | 243 | def get_path(self): 244 | """Returns the full path to the file without name and without 245 | trailing slash 246 | 247 | :returns: path to the file 248 | """ 249 | return os.path.dirname(self.path) 250 | 251 | def get_size(self): 252 | """Returns the size of the file 253 | 254 | :returns: size of the file 255 | """ 256 | if '{DAV:}getcontentlength' in self.attributes: 257 | return int(self.attributes['{DAV:}getcontentlength']) 258 | return None 259 | 260 | def get_etag(self): 261 | """Returns the file etag 262 | 263 | :returns: file etag 264 | """ 265 | return self.attributes['{DAV:}getetag'] 266 | 267 | def get_content_type(self): 268 | """Returns the file content type 269 | 270 | :returns: file content type 271 | """ 272 | if '{DAV:}getcontenttype' in self.attributes: 273 | return self.attributes['{DAV:}getcontenttype'] 274 | 275 | if self.is_dir(): 276 | return 'httpd/unix-directory' 277 | 278 | return None 279 | 280 | def get_last_modified(self): 281 | """Returns the last modified time 282 | 283 | :returns: last modified time 284 | :rtype: datetime object 285 | """ 286 | return datetime.datetime.strptime( 287 | self.attributes['{DAV:}getlastmodified'], 288 | self._DATE_FORMAT 289 | ) 290 | 291 | def is_dir(self): 292 | """Returns whether the file info is a directory 293 | 294 | :returns: True if it is a directory, False otherwise 295 | """ 296 | return self.file_type != 'file' 297 | 298 | def __str__(self): 299 | return 'File(path=%s,file_type=%s,attributes=%s)' % \ 300 | (self.path, self.file_type, self.attributes) 301 | 302 | def __repr__(self): 303 | return self.__str__() 304 | 305 | 306 | class Client(object): 307 | """ownCloud client""" 308 | 309 | OCS_BASEPATH = 'ocs/v1.php/' 310 | OCS_SERVICE_SHARE = 'apps/files_sharing/api/v1' 311 | OCS_SERVICE_PRIVATEDATA = 'privatedata' 312 | OCS_SERVICE_CLOUD = 'cloud' 313 | 314 | # constants from lib/public/constants.php 315 | OCS_PERMISSION_READ = 1 316 | OCS_PERMISSION_UPDATE = 2 317 | OCS_PERMISSION_CREATE = 4 318 | OCS_PERMISSION_DELETE = 8 319 | OCS_PERMISSION_SHARE = 16 320 | OCS_PERMISSION_ALL = 31 321 | # constants from lib/public/share.php 322 | OCS_SHARE_TYPE_USER = 0 323 | OCS_SHARE_TYPE_GROUP = 1 324 | OCS_SHARE_TYPE_LINK = 3 325 | OCS_SHARE_TYPE_REMOTE = 6 326 | 327 | def __init__(self, url, **kwargs): 328 | """Instantiates a client 329 | 330 | :param url: URL of the target ownCloud instance 331 | :param verify_certs: True (default) to verify SSL certificates, False otherwise 332 | :param dav_endpoint_version: None (default) to force using a specific endpoint version 333 | instead of relying on capabilities 334 | :param debug: set to True to print debugging messages to stdout, defaults to False 335 | """ 336 | if not url.endswith('/'): 337 | url += '/' 338 | 339 | self.url = url 340 | self._session = None 341 | self._debug = kwargs.get('debug', False) 342 | self._verify_certs = kwargs.get('verify_certs', True) 343 | self._dav_endpoint_version = kwargs.get('dav_endpoint_version', True) 344 | 345 | self._capabilities = None 346 | self._version = None 347 | 348 | def login(self, user_id, password): 349 | """Authenticate to ownCloud. 350 | This will create a session on the server. 351 | 352 | :param user_id: user id 353 | :param password: password 354 | :raises: HTTPResponseError in case an HTTP error status was returned 355 | """ 356 | 357 | self._session = requests.session() 358 | self._session.verify = self._verify_certs 359 | self._session.auth = (user_id, password) 360 | 361 | try: 362 | self._update_capabilities() 363 | 364 | url_components = parse.urlparse(self.url) 365 | if self._dav_endpoint_version == 1: 366 | self._davpath = url_components.path + 'remote.php/dav/files/' + parse.quote(user_id) 367 | self._webdav_url = self.url + 'remote.php/dav/files/' + parse.quote(user_id) 368 | else: 369 | self._davpath = url_components.path + 'remote.php/webdav' 370 | self._webdav_url = self.url + 'remote.php/webdav' 371 | 372 | except HTTPResponseError as e: 373 | self._session.close() 374 | self._session = None 375 | raise e 376 | 377 | def logout(self): 378 | """Log out the authenticated user and close the session. 379 | 380 | :returns: True if the operation succeeded, False otherwise 381 | :raises: HTTPResponseError in case an HTTP error status was returned 382 | """ 383 | # TODO actual logout ? 384 | self._session.close() 385 | return True 386 | 387 | def anon_login(self, folder_token, folder_password=''): 388 | self._session = requests.session() 389 | self._session.verify = self._verify_certs 390 | self._session.auth = (folder_token, folder_password) 391 | 392 | url_components = parse.urlparse(self.url) 393 | self._davpath = url_components.path + 'public.php/webdav' 394 | self._webdav_url = self.url + 'public.php/webdav' 395 | 396 | @classmethod 397 | def from_public_link(cls, public_link, folder_password='', **kwargs): 398 | public_link_components = parse.urlparse(public_link) 399 | url = public_link_components.scheme + '://' + public_link_components.hostname 400 | if public_link_components.port: 401 | url += ":" + public_link_components.port 402 | folder_token = public_link_components.path.split('/')[-1] 403 | anon_session = cls(url, **kwargs) 404 | anon_session.anon_login(folder_token, folder_password=folder_password) 405 | return anon_session 406 | 407 | def drop_file(self, file_name): 408 | """ Convenience wrapper for put_file """ 409 | destination = '/' + os.path.basename(file_name) 410 | return self.put_file(destination, file_name) 411 | 412 | def file_info(self, path, properties=None): 413 | """Returns the file info for the given remote file 414 | 415 | :param path: path to the remote file 416 | :param properties: a list of properties to request (optional) 417 | :returns: file info 418 | :rtype: :class:`FileInfo` object or `None` if file 419 | was not found 420 | :raises: HTTPResponseError in case an HTTP error status was returned 421 | """ 422 | if properties: 423 | root = ET.Element('d:propfind', 424 | { 425 | 'xmlns:d': "DAV:", 426 | 'xmlns:nc': "http://nextcloud.org/ns", 427 | 'xmlns:oc': "http://owncloud.org/ns" 428 | }) 429 | prop = ET.SubElement(root, 'd:prop') 430 | for p in properties: 431 | ET.SubElement(prop, p) 432 | data = ET.tostring(root) 433 | else: 434 | data = None 435 | res = self._make_dav_request('PROPFIND', path, headers={'Depth': '0'}, data=data) 436 | if res: 437 | return res[0] 438 | return None 439 | 440 | def list(self, path, depth=1, properties=None): 441 | """Returns the listing/contents of the given remote directory 442 | 443 | :param path: path to the remote directory 444 | :param depth: depth of the listing, integer or "infinity" 445 | :param properties: a list of properties to request (optional) 446 | :returns: directory listing 447 | :rtype: array of :class:`FileInfo` objects 448 | :raises: HTTPResponseError in case an HTTP error status was returned 449 | """ 450 | if not path.endswith('/'): 451 | path += '/' 452 | 453 | headers = {} 454 | if isinstance(depth, int) or depth == "infinity": 455 | headers['Depth'] = str(depth) 456 | 457 | if properties: 458 | root = ET.Element('d:propfind', 459 | { 460 | 'xmlns:d': "DAV:", 461 | 'xmlns:nc': "http://nextcloud.org/ns", 462 | 'xmlns:oc': "http://owncloud.org/ns" 463 | }) 464 | prop = ET.SubElement(root, 'd:prop') 465 | for p in properties: 466 | ET.SubElement(prop, p) 467 | data = ET.tostring(root) 468 | else: 469 | data = None 470 | 471 | res = self._make_dav_request('PROPFIND', path, headers=headers, data=data) 472 | # first one is always the root, remove it from listing 473 | if res: 474 | return res[1:] 475 | return None 476 | 477 | def get_file_contents(self, path): 478 | """Returns the contents of a remote file 479 | 480 | :param path: path to the remote file 481 | :returns: file contents 482 | :rtype: binary data 483 | :raises: HTTPResponseError in case an HTTP error status was returned 484 | """ 485 | path = self._normalize_path(path) 486 | res = self._session.get( 487 | self._webdav_url + parse.quote(self._encode_string(path)) 488 | ) 489 | if res.status_code == 200: 490 | return res.content 491 | elif res.status_code >= 400: 492 | raise HTTPResponseError(res) 493 | return False 494 | 495 | def get_file(self, remote_path, local_file=None): 496 | """Downloads a remote file 497 | 498 | :param remote_path: path to the remote file 499 | :param local_file: optional path to the local file. If none specified, 500 | the file will be downloaded into the current directory 501 | :returns: True if the operation succeeded, False otherwise 502 | :raises: HTTPResponseError in case an HTTP error status was returned 503 | """ 504 | remote_path = self._normalize_path(remote_path) 505 | res = self._session.get( 506 | self._webdav_url + parse.quote(self._encode_string(remote_path)), 507 | stream=True 508 | ) 509 | if res.status_code == 200: 510 | if local_file is None: 511 | # use downloaded file name from Content-Disposition 512 | # local_file = res.headers['content-disposition'] 513 | local_file = os.path.basename(remote_path) 514 | 515 | file_handle = open(local_file, 'wb', 8192) 516 | for chunk in res.iter_content(8192): 517 | file_handle.write(chunk) 518 | file_handle.close() 519 | return True 520 | elif res.status_code >= 400: 521 | raise HTTPResponseError(res) 522 | return False 523 | 524 | def get_directory_as_zip(self, remote_path, local_file): 525 | """Downloads a remote directory as zip 526 | 527 | :param remote_path: path to the remote directory to download 528 | :param local_file: path and name of the target local file 529 | :returns: True if the operation succeeded, False otherwise 530 | :raises: HTTPResponseError in case an HTTP error status was returned 531 | """ 532 | remote_path = self._normalize_path(remote_path) 533 | url = self.url + 'index.php/apps/files/ajax/download.php?dir=' \ 534 | + parse.quote(remote_path) 535 | res = self._session.get(url, stream=True) 536 | if res.status_code == 200: 537 | if local_file is None: 538 | # use downloaded file name from Content-Disposition 539 | # targetFile = res.headers['content-disposition'] 540 | local_file = os.path.basename(remote_path) 541 | 542 | file_handle = open(local_file, 'wb', 8192) 543 | for chunk in res.iter_content(8192): 544 | file_handle.write(chunk) 545 | file_handle.close() 546 | return True 547 | elif res.status_code >= 400: 548 | raise HTTPResponseError(res) 549 | return False 550 | 551 | def put_file_contents(self, remote_path, data): 552 | """Write data into a remote file 553 | 554 | :param remote_path: path of the remote file 555 | :param data: data to write into the remote file 556 | :returns: True if the operation succeeded, False otherwise 557 | :raises: HTTPResponseError in case an HTTP error status was returned 558 | """ 559 | return self._make_dav_request('PUT', remote_path, data=data) 560 | 561 | def put_file(self, remote_path, local_source_file, **kwargs): 562 | """Upload a file 563 | 564 | :param remote_path: path to the target file. A target directory can 565 | also be specified instead by appending a "/" 566 | :param local_source_file: path to the local file to upload 567 | :param chunked: (optional) use file chunking (defaults to True) 568 | :param chunk_size: (optional) chunk size in bytes, defaults to 10 MB 569 | :param keep_mtime: (optional) also update the remote file to the same 570 | mtime as the local one, defaults to True 571 | :returns: True if the operation succeeded, False otherwise 572 | :raises: HTTPResponseError in case an HTTP error status was returned 573 | """ 574 | if kwargs.get('chunked', True): 575 | return self._put_file_chunked( 576 | remote_path, 577 | local_source_file, 578 | **kwargs 579 | ) 580 | 581 | stat_result = os.stat(local_source_file) 582 | 583 | headers = {} 584 | if kwargs.get('keep_mtime', True): 585 | headers['X-OC-MTIME'] = str(int(stat_result.st_mtime)) 586 | 587 | if remote_path[-1] == '/': 588 | remote_path += os.path.basename(local_source_file) 589 | file_handle = open(local_source_file, 'rb', 8192) 590 | res = self._make_dav_request( 591 | 'PUT', 592 | remote_path, 593 | data=file_handle, 594 | headers=headers 595 | ) 596 | file_handle.close() 597 | return res 598 | 599 | def put_directory(self, target_path, local_directory, **kwargs): 600 | """Upload a directory with all its contents 601 | 602 | :param target_path: path of the directory to upload into 603 | :param local_directory: path to the local directory to upload 604 | :param \*\*kwargs: optional arguments that ``put_file`` accepts 605 | :returns: True if the operation succeeded, False otherwise 606 | :raises: HTTPResponseError in case an HTTP error status was returned 607 | """ 608 | target_path = self._normalize_path(target_path) 609 | if not target_path.endswith('/'): 610 | target_path += '/' 611 | gathered_files = [] 612 | 613 | if not local_directory.endswith('/'): 614 | local_directory += '/' 615 | 616 | basedir = os.path.basename(local_directory[0: -1]) + '/' 617 | # gather files to upload 618 | for path, _, files in os.walk(local_directory): 619 | gathered_files.append( 620 | (path, basedir + path[len(local_directory):], files) 621 | ) 622 | 623 | for path, remote_path, files in gathered_files: 624 | self.mkdir(target_path + remote_path + '/') 625 | for name in files: 626 | if not self.put_file(target_path + remote_path + '/', 627 | path + '/' + name, **kwargs): 628 | return False 629 | return True 630 | 631 | def _put_file_chunked(self, remote_path, local_source_file, **kwargs): 632 | """Uploads a file using chunks. If the file is smaller than 633 | ``chunk_size`` it will be uploaded directly. 634 | 635 | :param remote_path: path to the target file. A target directory can 636 | also be specified instead by appending a "/" 637 | :param local_source_file: path to the local file to upload 638 | :param \*\*kwargs: optional arguments that ``put_file`` accepts 639 | :returns: True if the operation succeeded, False otherwise 640 | :raises: HTTPResponseError in case an HTTP error status was returned 641 | """ 642 | chunk_size = kwargs.get('chunk_size', 10 * 1024 * 1024) 643 | result = True 644 | transfer_id = int(time.time()) 645 | 646 | remote_path = self._normalize_path(remote_path) 647 | if remote_path.endswith('/'): 648 | remote_path += os.path.basename(local_source_file) 649 | 650 | stat_result = os.stat(local_source_file) 651 | 652 | file_handle = open(local_source_file, 'rb', 8192) 653 | file_handle.seek(0, os.SEEK_END) 654 | size = file_handle.tell() 655 | file_handle.seek(0) 656 | 657 | headers = {} 658 | if kwargs.get('keep_mtime', True): 659 | headers['X-OC-MTIME'] = str(int(stat_result.st_mtime)) 660 | 661 | if size == 0: 662 | return self._make_dav_request( 663 | 'PUT', 664 | remote_path, 665 | data='', 666 | headers=headers 667 | ) 668 | 669 | chunk_count = int(math.ceil(float(size) / float(chunk_size))) 670 | 671 | if chunk_count > 1: 672 | headers['OC-CHUNKED'] = '1' 673 | 674 | for chunk_index in range(0, int(chunk_count)): 675 | data = file_handle.read(chunk_size) 676 | if chunk_count > 1: 677 | chunk_name = '%s-chunking-%s-%i-%i' % \ 678 | (remote_path, transfer_id, chunk_count, 679 | chunk_index) 680 | else: 681 | chunk_name = remote_path 682 | 683 | if not self._make_dav_request( 684 | 'PUT', 685 | chunk_name, 686 | data=data, 687 | headers=headers 688 | ): 689 | result = False 690 | break 691 | 692 | file_handle.close() 693 | return result 694 | 695 | def mkdir(self, path): 696 | """Creates a remote directory 697 | 698 | :param path: path to the remote directory to create 699 | :returns: True if the operation succeeded, False otherwise 700 | :raises: HTTPResponseError in case an HTTP error status was returned 701 | """ 702 | if not path.endswith('/'): 703 | path += '/' 704 | return self._make_dav_request('MKCOL', path) 705 | 706 | def delete(self, path): 707 | """Deletes a remote file or directory 708 | 709 | :param path: path to the file or directory to delete 710 | :returns: True if the operation succeeded, False otherwise 711 | :raises: HTTPResponseError in case an HTTP error status was returned 712 | """ 713 | return self._make_dav_request('DELETE', path) 714 | 715 | def list_open_remote_share(self): 716 | """List all pending remote shares 717 | 718 | :returns: array of pending remote shares 719 | :raises: HTTPResponseError in case an HTTP error status was returned 720 | """ 721 | 722 | res = self._make_ocs_request( 723 | 'GET', 724 | self.OCS_SERVICE_SHARE, 725 | 'remote_shares/pending' 726 | ) 727 | if res.status_code == 200: 728 | tree = ET.fromstring(res.content) 729 | self._check_ocs_status(tree) 730 | shares = [] 731 | for element in tree.find('data').iter('element'): 732 | share_attr = {} 733 | for child in element: 734 | key = child.tag 735 | value = child.text 736 | share_attr[key] = value 737 | shares.append(share_attr) 738 | return shares 739 | raise HTTPResponseError(res) 740 | 741 | def accept_remote_share(self, share_id): 742 | """Accepts a remote share 743 | 744 | :param share_id: Share ID (int) 745 | :returns: True if the operation succeeded, False otherwise 746 | :raises: HTTPResponseError in case an HTTP error status was returned 747 | """ 748 | if not isinstance(share_id, int): 749 | return False 750 | 751 | res = self._make_ocs_request( 752 | 'POST', 753 | self.OCS_SERVICE_SHARE, 754 | 'remote_shares/pending/' + str(share_id) 755 | ) 756 | if res.status_code == 200: 757 | return res 758 | raise HTTPResponseError(res) 759 | 760 | def decline_remote_share(self, share_id): 761 | """Declines a remote share 762 | 763 | :param share_id: Share ID (int) 764 | :returns: True if the operation succeeded, False otherwise 765 | :raises: HTTPResponseError in case an HTTP error status was returned 766 | """ 767 | if not isinstance(share_id, int): 768 | return False 769 | 770 | res = self._make_ocs_request( 771 | 'DELETE', 772 | self.OCS_SERVICE_SHARE, 773 | 'remote_shares/pending/' + str(share_id) 774 | ) 775 | if res.status_code == 200: 776 | return res 777 | raise HTTPResponseError(res) 778 | 779 | def delete_share(self, share_id): 780 | """Unshares a file or directory 781 | 782 | :param share_id: Share ID (int) 783 | :returns: True if the operation succeeded, False otherwise 784 | :raises: HTTPResponseError in case an HTTP error status was returned 785 | """ 786 | if not isinstance(share_id, int): 787 | return False 788 | 789 | res = self._make_ocs_request( 790 | 'DELETE', 791 | self.OCS_SERVICE_SHARE, 792 | 'shares/' + str(share_id) 793 | ) 794 | if res.status_code == 200: 795 | return res 796 | raise HTTPResponseError(res) 797 | 798 | def update_share(self, share_id, **kwargs): 799 | """Updates a given share 800 | 801 | :param share_id: (int) Share ID 802 | :param perms: (int) update permissions (see share_file_with_user() below) 803 | :param password: (string) updated password for public link Share 804 | :param public_upload: (boolean) enable/disable public upload for public shares 805 | :returns: True if the operation succeeded, False otherwise 806 | :raises: HTTPResponseError in case an HTTP error status was returned 807 | """ 808 | 809 | perms = kwargs.get('perms', None) 810 | password = kwargs.get('password', None) 811 | public_upload = kwargs.get('public_upload', None) 812 | if (isinstance(perms, int)) and (perms > self.OCS_PERMISSION_ALL): 813 | perms = None 814 | if not (perms or password or (public_upload is not None)): 815 | return False 816 | if not isinstance(share_id, int): 817 | return False 818 | 819 | data = {} 820 | if perms: 821 | data['permissions'] = perms 822 | if isinstance(password, six.string_types): 823 | data['password'] = password 824 | if (public_upload is not None) and (isinstance(public_upload, bool)): 825 | data['publicUpload'] = str(public_upload).lower() 826 | 827 | res = self._make_ocs_request( 828 | 'PUT', 829 | self.OCS_SERVICE_SHARE, 830 | 'shares/' + str(share_id), 831 | data=data 832 | ) 833 | if res.status_code == 200: 834 | return True 835 | raise HTTPResponseError(res) 836 | 837 | def move(self, remote_path_source, remote_path_target): 838 | """Moves a remote file or directory 839 | 840 | :param remote_path_source: source file or folder to move 841 | :param remote_path_target: target file to which to move 842 | the source file. A target directory can also be specified 843 | instead by appending a "/" 844 | :returns: True if the operation succeeded, False otherwise 845 | :raises: HTTPResponseError in case an HTTP error status was returned 846 | """ 847 | 848 | return self._webdav_move_copy(remote_path_source, remote_path_target, 849 | "MOVE") 850 | 851 | def copy(self, remote_path_source, remote_path_target): 852 | """Copies a remote file or directory 853 | 854 | :param remote_path_source: source file or folder to copy 855 | :param remote_path_target: target file to which to copy 856 | 857 | :returns: True if the operation succeeded, False otherwise 858 | :raises: HTTPResponseError in case an HTTP error status was returned 859 | """ 860 | return self._webdav_move_copy(remote_path_source, remote_path_target, 861 | "COPY") 862 | 863 | def share_file_with_link(self, path, **kwargs): 864 | """Shares a remote file with link 865 | 866 | :param path: path to the remote file to share 867 | :param perms (optional): permission of the shared object 868 | defaults to read only (1) 869 | :param public_upload (optional): allows users to upload files or folders 870 | :param password (optional): sets a password 871 | https://doc.owncloud.com/server/next/admin_manual/configuration/files/file_sharing_configuration.html 872 | :param name (optional): display name for the link 873 | :returns: instance of :class:`ShareInfo` with the share info 874 | or False if the operation failed 875 | :raises: HTTPResponseError in case an HTTP error status was returned 876 | """ 877 | perms = kwargs.get('perms', None) 878 | public_upload = kwargs.get('public_upload', 'false') 879 | password = kwargs.get('password', None) 880 | name = kwargs.get('name', None) 881 | 882 | path = self._normalize_path(path) 883 | post_data = { 884 | 'shareType': self.OCS_SHARE_TYPE_LINK, 885 | 'path': self._encode_string(path), 886 | } 887 | if (public_upload is not None) and (isinstance(public_upload, bool)): 888 | post_data['publicUpload'] = str(public_upload).lower() 889 | if isinstance(password, six.string_types): 890 | post_data['password'] = password 891 | if name is not None: 892 | post_data['name'] = self._encode_string(name) 893 | if perms: 894 | post_data['permissions'] = perms 895 | 896 | res = self._make_ocs_request( 897 | 'POST', 898 | self.OCS_SERVICE_SHARE, 899 | 'shares', 900 | data=post_data 901 | ) 902 | if res.status_code == 200: 903 | tree = ET.fromstring(res.content) 904 | self._check_ocs_status(tree) 905 | data_el = tree.find('data') 906 | return ShareInfo( 907 | { 908 | 'id': data_el.find('id').text, 909 | 'path': path, 910 | 'url': data_el.find('url').text, 911 | 'token': data_el.find('token').text, 912 | 'name': data_el.find('name').text 913 | } 914 | ) 915 | raise HTTPResponseError(res) 916 | 917 | def is_shared(self, path): 918 | """Checks whether a path is already shared 919 | 920 | :param path: path to the share to be checked 921 | :returns: True if the path is already shared, else False 922 | :raises: HTTPResponseError in case an HTTP error status was returned 923 | """ 924 | # make sure that the path exist - if not, raise HTTPResponseError 925 | self.file_info(path) 926 | try: 927 | result = self.get_shares(path) 928 | if result: 929 | return len(result) > 0 930 | except OCSResponseError as e: 931 | if e.status_code != 404: 932 | raise e 933 | return False 934 | return False 935 | 936 | def get_share(self, share_id): 937 | """Returns share information about known share 938 | 939 | :param share_id: id of the share to be checked 940 | :returns: instance of ShareInfo class 941 | :raises: ResponseError in case an HTTP error status was returned 942 | """ 943 | if (share_id is None) or not (isinstance(share_id, int)): 944 | return None 945 | 946 | res = self._make_ocs_request( 947 | 'GET', 948 | self.OCS_SERVICE_SHARE, 949 | 'shares/' + str(share_id) 950 | ) 951 | if res.status_code == 200: 952 | tree = ET.fromstring(res.content) 953 | self._check_ocs_status(tree) 954 | return self._get_shareinfo(tree.find('data').find('element')) 955 | raise HTTPResponseError(res) 956 | 957 | def get_shares(self, path='', **kwargs): 958 | """Returns array of shares 959 | 960 | :param path: path to the share to be checked 961 | :param reshares: (optional, boolean) returns not only the shares from 962 | the current user but all shares from the given file (default: False) 963 | :param subfiles: (optional, boolean) returns all shares within 964 | a folder, given that path defines a folder (default: False) 965 | :param shared_with_me: (optional, boolean) returns all shares which are 966 | shared with me (default: False) 967 | :returns: array of shares ShareInfo instances or empty array if the operation failed 968 | :raises: HTTPResponseError in case an HTTP error status was returned 969 | """ 970 | if not (isinstance(path, six.string_types)): 971 | return None 972 | 973 | data = 'shares' 974 | if path != '': 975 | data += '?' 976 | path = self._encode_string(self._normalize_path(path)) 977 | args = {'path': path} 978 | reshares = kwargs.get('reshares', False) 979 | if isinstance(reshares, bool) and reshares: 980 | args['reshares'] = reshares 981 | subfiles = kwargs.get('subfiles', False) 982 | if isinstance(subfiles, bool) and subfiles: 983 | args['subfiles'] = str(subfiles).lower() 984 | 985 | shared_with_me = kwargs.get('shared_with_me', False) 986 | if isinstance(shared_with_me, bool) and shared_with_me: 987 | args['shared_with_me'] = "true" 988 | del args['path'] 989 | 990 | data += parse.urlencode(args) 991 | 992 | res = self._make_ocs_request( 993 | 'GET', 994 | self.OCS_SERVICE_SHARE, 995 | data 996 | ) 997 | 998 | if res.status_code == 200: 999 | tree = ET.fromstring(res.content) 1000 | self._check_ocs_status(tree) 1001 | shares = [] 1002 | for element in tree.find('data').iter('element'): 1003 | '''share_attr = {} 1004 | for child in element: 1005 | key = child.tag 1006 | value = child.text 1007 | share_attr[key] = value 1008 | shares.append(share_attr)''' 1009 | shares.append(self._get_shareinfo(element)) 1010 | return shares 1011 | raise HTTPResponseError(res) 1012 | 1013 | def create_user(self, user_name, initial_password): 1014 | """Create a new user with an initial password via provisioning API. 1015 | It is not an error, if the user already existed before. 1016 | If you get back an error 999, then the provisioning API is not enabled. 1017 | 1018 | :param user_name: name of user to be created 1019 | :param initial_password: password for user being created 1020 | :returns: True on success 1021 | :raises: HTTPResponseError in case an HTTP error status was returned 1022 | 1023 | """ 1024 | res = self._make_ocs_request( 1025 | 'POST', 1026 | self.OCS_SERVICE_CLOUD, 1027 | 'users', 1028 | data={'password': initial_password, 'userid': user_name} 1029 | ) 1030 | 1031 | # We get 200 when the user was just created. 1032 | if res.status_code == 200: 1033 | tree = ET.fromstring(res.content) 1034 | self._check_ocs_status(tree, [100]) 1035 | return True 1036 | 1037 | raise HTTPResponseError(res) 1038 | 1039 | def delete_user(self, user_name): 1040 | """Deletes a user via provisioning API. 1041 | If you get back an error 999, then the provisioning API is not enabled. 1042 | 1043 | :param user_name: name of user to be deleted 1044 | :returns: True on success 1045 | :raises: HTTPResponseError in case an HTTP error status was returned 1046 | 1047 | """ 1048 | res = self._make_ocs_request( 1049 | 'DELETE', 1050 | self.OCS_SERVICE_CLOUD, 1051 | 'users/' + user_name 1052 | ) 1053 | 1054 | # We get 200 when the user was deleted. 1055 | if res.status_code == 200: 1056 | return True 1057 | 1058 | raise HTTPResponseError(res) 1059 | 1060 | def user_exists(self, user_name): 1061 | """Checks a user via provisioning API. 1062 | If you get back an error 999, then the provisioning API is not enabled. 1063 | 1064 | :param user_name: name of user to be checked 1065 | :returns: True if user found 1066 | 1067 | """ 1068 | users = self.search_users(user_name) 1069 | 1070 | return user_name in users 1071 | 1072 | def search_users(self, user_name): 1073 | """Searches for users via provisioning API. 1074 | If you get back an error 999, then the provisioning API is not enabled. 1075 | 1076 | :param user_name: name of user to be searched for 1077 | :returns: list of usernames that contain user_name as substring 1078 | :raises: HTTPResponseError in case an HTTP error status was returned 1079 | 1080 | """ 1081 | action_path = 'users' 1082 | if user_name: 1083 | action_path += '?search={}'.format(user_name) 1084 | 1085 | res = self._make_ocs_request( 1086 | 'GET', 1087 | self.OCS_SERVICE_CLOUD, 1088 | action_path 1089 | ) 1090 | 1091 | if res.status_code == 200: 1092 | tree = ET.fromstring(res.content) 1093 | users = [x.text for x in tree.findall('data/users/element')] 1094 | 1095 | return users 1096 | 1097 | raise HTTPResponseError(res) 1098 | 1099 | def get_users(self): 1100 | """Get users via provisioning API. 1101 | If you get back an error 999, then the provisioning API is not enabled. 1102 | 1103 | :returns: list of usernames 1104 | :raises: HTTPResponseError in case an HTTP error status was returned 1105 | 1106 | """ 1107 | return self.search_users('') 1108 | 1109 | def set_user_attribute(self, user_name, key, value): 1110 | """Sets a user attribute 1111 | 1112 | :param user_name: name of user to modify 1113 | :param key: key of the attribute to set 1114 | :param value: value to set 1115 | :returns: True if the operation succeeded, False otherwise 1116 | :raises: HTTPResponseError in case an HTTP error status was returned 1117 | """ 1118 | 1119 | res = self._make_ocs_request( 1120 | 'PUT', 1121 | self.OCS_SERVICE_CLOUD, 1122 | 'users/' + parse.quote(user_name), 1123 | data={'key': self._encode_string(key), 1124 | 'value': self._encode_string(value)} 1125 | ) 1126 | 1127 | if res.status_code == 200: 1128 | tree = ET.fromstring(res.content) 1129 | self._check_ocs_status(tree, [100]) 1130 | return True 1131 | raise HTTPResponseError(res) 1132 | 1133 | def add_user_to_group(self, user_name, group_name): 1134 | """Adds a user to a group. 1135 | 1136 | :param user_name: name of user to be added 1137 | :param group_name: name of group user is to be added to 1138 | :returns: True if user added 1139 | :raises: HTTPResponseError in case an HTTP error status was returned 1140 | 1141 | """ 1142 | 1143 | res = self._make_ocs_request( 1144 | 'POST', 1145 | self.OCS_SERVICE_CLOUD, 1146 | 'users/' + user_name + '/groups', 1147 | data={'groupid': group_name} 1148 | ) 1149 | 1150 | if res.status_code == 200: 1151 | tree = ET.fromstring(res.content) 1152 | self._check_ocs_status(tree, [100]) 1153 | return True 1154 | 1155 | raise HTTPResponseError(res) 1156 | 1157 | def get_user_groups(self, user_name): 1158 | """Get a list of groups associated to a user. 1159 | 1160 | :param user_name: name of user to list groups 1161 | :returns: list of groups 1162 | :raises: HTTPResponseError in case an HTTP error status was returned 1163 | 1164 | """ 1165 | 1166 | res = self._make_ocs_request( 1167 | 'GET', 1168 | self.OCS_SERVICE_CLOUD, 1169 | 'users/' + user_name + '/groups', 1170 | ) 1171 | 1172 | if res.status_code == 200: 1173 | tree = ET.fromstring(res.content) 1174 | self._check_ocs_status(tree, [100]) 1175 | return [group.text for group in tree.find('data/groups')] 1176 | 1177 | raise HTTPResponseError(res) 1178 | 1179 | def user_is_in_group(self, user_name, group_name): 1180 | """Checks if a user is in a group 1181 | 1182 | :param user_name: name of user 1183 | :param group_name: name of group 1184 | :returns: True if user is in group 1185 | :raises: HTTPResponseError in case an HTTP error status was returned 1186 | """ 1187 | return group_name in self.get_user_groups(user_name) 1188 | 1189 | def get_user(self, user_name): 1190 | """Retrieves information about a user 1191 | 1192 | :param user_name: name of user to query 1193 | 1194 | :returns: Dictionary of information about user 1195 | :raises: ResponseError in case an HTTP error status was returned 1196 | """ 1197 | res = self._make_ocs_request( 1198 | 'GET', 1199 | self.OCS_SERVICE_CLOUD, 1200 | 'users/' + parse.quote(user_name), 1201 | data={} 1202 | ) 1203 | 1204 | tree = ET.fromstring(res.content) 1205 | self._check_ocs_status(tree) 1206 | # 100ok 1207 | # 1208 | # frank@example.org0true 1209 | # 1210 | # 1211 | 1212 | data_element = tree.find('data') 1213 | return self._xml_to_dict(data_element) 1214 | 1215 | def remove_user_from_group(self, user_name, group_name): 1216 | """Removes a user from a group. 1217 | 1218 | :param user_name: name of user to be removed 1219 | :param group_name: name of group user is to be removed from 1220 | :returns: True if user removed 1221 | :raises: HTTPResponseError in case an HTTP error status was returned 1222 | 1223 | """ 1224 | res = self._make_ocs_request( 1225 | 'DELETE', 1226 | self.OCS_SERVICE_CLOUD, 1227 | 'users/' + user_name + '/groups', 1228 | data={'groupid': group_name} 1229 | ) 1230 | 1231 | if res.status_code == 200: 1232 | tree = ET.fromstring(res.content) 1233 | self._check_ocs_status(tree, [100]) 1234 | return True 1235 | 1236 | raise HTTPResponseError(res) 1237 | 1238 | def add_user_to_subadmin_group(self, user_name, group_name): 1239 | """Adds a user to a subadmin group. 1240 | 1241 | :param user_name: name of user to be added to subadmin group 1242 | :param group_name: name of subadmin group 1243 | :returns: True if user added 1244 | :raises: HTTPResponseError in case an HTTP error status was returned 1245 | 1246 | """ 1247 | 1248 | res = self._make_ocs_request( 1249 | 'POST', 1250 | self.OCS_SERVICE_CLOUD, 1251 | 'users/' + user_name + '/subadmins', 1252 | data={'groupid': group_name} 1253 | ) 1254 | 1255 | if res.status_code == 200: 1256 | tree = ET.fromstring(res.content) 1257 | self._check_ocs_status(tree, [100, 103]) 1258 | return True 1259 | 1260 | raise HTTPResponseError(res) 1261 | 1262 | def get_user_subadmin_groups(self, user_name): 1263 | """Get a list of subadmin groups associated to a user. 1264 | 1265 | :param user_name: name of user 1266 | :returns: list of subadmin groups 1267 | :raises: HTTPResponseError in case an HTTP error status was returned 1268 | 1269 | """ 1270 | 1271 | res = self._make_ocs_request( 1272 | 'GET', 1273 | self.OCS_SERVICE_CLOUD, 1274 | 'users/' + user_name + '/subadmins', 1275 | ) 1276 | 1277 | if res.status_code == 200: 1278 | tree = ET.fromstring(res.content) 1279 | self._check_ocs_status(tree, [100]) 1280 | 1281 | groups = tree.find('data') 1282 | 1283 | return groups 1284 | 1285 | raise HTTPResponseError(res) 1286 | 1287 | def user_is_in_subadmin_group(self, user_name, group_name): 1288 | """Checks if a user is in a subadmin group 1289 | 1290 | :param user_name: name of user 1291 | :param group_name: name of subadmin group 1292 | :returns: True if user is in subadmin group 1293 | :raises: HTTPResponseError in case an HTTP error status was returned 1294 | """ 1295 | return group_name in self.get_user_subadmin_groups(user_name) 1296 | 1297 | def share_file_with_user(self, path, user, **kwargs): 1298 | """Shares a remote file with specified user 1299 | 1300 | :param path: path to the remote file to share 1301 | :param user: name of the user whom we want to share a file/folder 1302 | :param perms (optional): permissions of the shared object 1303 | defaults to read only (1) 1304 | https://doc.owncloud.com/server/next/admin_manual/configuration/files/file_sharing_configuration.html 1305 | :param remote_user (optional): True if it is a federated users 1306 | defaults to False if it is a local user 1307 | :returns: instance of :class:`ShareInfo` with the share info 1308 | or False if the operation failed 1309 | :raises: HTTPResponseError in case an HTTP error status was returned 1310 | """ 1311 | remote_user = kwargs.get('remote_user', False) 1312 | perms = kwargs.get('perms', self.OCS_PERMISSION_READ) 1313 | if (((not isinstance(perms, int)) or (perms > self.OCS_PERMISSION_ALL)) 1314 | or ((not isinstance(user, six.string_types)) or (user == ''))): 1315 | return False 1316 | 1317 | if remote_user and (not user.endswith('/')): 1318 | user = user + '/' 1319 | path = self._normalize_path(path) 1320 | post_data = { 1321 | 'shareType': self.OCS_SHARE_TYPE_REMOTE if remote_user else 1322 | self.OCS_SHARE_TYPE_USER, 1323 | 'shareWith': user, 1324 | 'path': self._encode_string(path), 1325 | 'permissions': perms 1326 | } 1327 | 1328 | res = self._make_ocs_request( 1329 | 'POST', 1330 | self.OCS_SERVICE_SHARE, 1331 | 'shares', 1332 | data=post_data 1333 | ) 1334 | 1335 | if self._debug: 1336 | print('OCS share_file request for file %s with permissions %i ' 1337 | 'returned: %i' % (path, perms, res.status_code)) 1338 | if res.status_code == 200: 1339 | tree = ET.fromstring(res.content) 1340 | self._check_ocs_status(tree) 1341 | data_el = tree.find('data') 1342 | return ShareInfo( 1343 | { 1344 | 'id': data_el.find('id').text, 1345 | 'path': path, 1346 | 'permissions': perms 1347 | } 1348 | ) 1349 | raise HTTPResponseError(res) 1350 | 1351 | def create_group(self, group_name): 1352 | """Create a new group via provisioning API. 1353 | If you get back an error 999, then the provisioning API is not enabled. 1354 | 1355 | :param group_name: name of group to be created 1356 | :returns: True if group created 1357 | :raises: HTTPResponseError in case an HTTP error status was returned 1358 | 1359 | """ 1360 | res = self._make_ocs_request( 1361 | 'POST', 1362 | self.OCS_SERVICE_CLOUD, 1363 | 'groups', 1364 | data={'groupid': group_name} 1365 | ) 1366 | 1367 | # We get 200 when the group was just created. 1368 | if res.status_code == 200: 1369 | tree = ET.fromstring(res.content) 1370 | self._check_ocs_status(tree, [100]) 1371 | return True 1372 | 1373 | raise HTTPResponseError(res) 1374 | 1375 | def delete_group(self, group_name): 1376 | """Delete a group via provisioning API. 1377 | If you get back an error 999, then the provisioning API is not enabled. 1378 | 1379 | :param group_name: name of group to be deleted 1380 | :returns: True if group deleted 1381 | :raises: HTTPResponseError in case an HTTP error status was returned 1382 | 1383 | """ 1384 | res = self._make_ocs_request( 1385 | 'DELETE', 1386 | self.OCS_SERVICE_CLOUD, 1387 | 'groups/' + group_name 1388 | ) 1389 | 1390 | # We get 200 when the group was just deleted. 1391 | if res.status_code == 200: 1392 | return True 1393 | 1394 | raise HTTPResponseError(res) 1395 | 1396 | def get_groups(self): 1397 | """Get groups via provisioning API. 1398 | If you get back an error 999, then the provisioning API is not enabled. 1399 | 1400 | :returns: list of groups 1401 | :raises: HTTPResponseError in case an HTTP error status was returned 1402 | 1403 | """ 1404 | res = self._make_ocs_request( 1405 | 'GET', 1406 | self.OCS_SERVICE_CLOUD, 1407 | 'groups' 1408 | ) 1409 | 1410 | if res.status_code == 200: 1411 | tree = ET.fromstring(res.content) 1412 | groups = [x.text for x in tree.findall('data/groups/element')] 1413 | 1414 | return groups 1415 | 1416 | raise HTTPResponseError(res) 1417 | 1418 | def get_group_members(self, group_name): 1419 | """Get group members via provisioning API. 1420 | If you get back an error 999, then the provisioning API is not enabled. 1421 | 1422 | :param group_name: name of group to list members 1423 | :returns: list of group members 1424 | :raises: HTTPResponseError in case an HTTP error status was returned 1425 | 1426 | """ 1427 | res = self._make_ocs_request( 1428 | 'GET', 1429 | self.OCS_SERVICE_CLOUD, 1430 | 'groups/' + group_name 1431 | ) 1432 | 1433 | if res.status_code == 200: 1434 | tree = ET.fromstring(res.content) 1435 | self._check_ocs_status(tree, [100]) 1436 | return [group.text for group in tree.find('data/users')] 1437 | 1438 | raise HTTPResponseError(res) 1439 | 1440 | def group_exists(self, group_name): 1441 | """Checks a group via provisioning API. 1442 | If you get back an error 999, then the provisioning API is not enabled. 1443 | 1444 | :param group_name: name of group to be checked 1445 | :returns: True if group exists 1446 | :raises: HTTPResponseError in case an HTTP error status was returned 1447 | 1448 | """ 1449 | res = self._make_ocs_request( 1450 | 'GET', 1451 | self.OCS_SERVICE_CLOUD, 1452 | 'groups?search=' + group_name 1453 | ) 1454 | 1455 | if res.status_code == 200: 1456 | tree = ET.fromstring(res.content) 1457 | 1458 | for code_el in tree.findall('data/groups/element'): 1459 | if code_el is not None and code_el.text == group_name: 1460 | return True 1461 | 1462 | return False 1463 | 1464 | raise HTTPResponseError(res) 1465 | 1466 | def share_file_with_group(self, path, group, **kwargs): 1467 | """Shares a remote file with specified group 1468 | 1469 | :param path: path to the remote file to share 1470 | :param group: name of the group with which we want to share a file/folder 1471 | :param perms (optional): permissions of the shared object 1472 | defaults to read only (1) 1473 | https://doc.owncloud.com/server/next/admin_manual/configuration/files/file_sharing_configuration.html 1474 | :returns: instance of :class:`ShareInfo` with the share info 1475 | or False if the operation failed 1476 | :raises: HTTPResponseError in case an HTTP error status was returned 1477 | """ 1478 | perms = kwargs.get('perms', self.OCS_PERMISSION_READ) 1479 | if (((not isinstance(perms, int)) or (perms > self.OCS_PERMISSION_ALL)) 1480 | or ((not isinstance(group, six.string_types)) or (group == ''))): 1481 | return False 1482 | 1483 | path = self._normalize_path(path) 1484 | post_data = {'shareType': self.OCS_SHARE_TYPE_GROUP, 1485 | 'shareWith': group, 1486 | 'path': path, 1487 | 'permissions': perms} 1488 | 1489 | res = self._make_ocs_request( 1490 | 'POST', 1491 | self.OCS_SERVICE_SHARE, 1492 | 'shares', 1493 | data=post_data 1494 | ) 1495 | if res.status_code == 200: 1496 | tree = ET.fromstring(res.content) 1497 | self._check_ocs_status(tree) 1498 | data_el = tree.find('data') 1499 | return ShareInfo( 1500 | { 1501 | 'id': data_el.find('id').text, 1502 | 'path': path, 1503 | 'permissions': perms 1504 | } 1505 | ) 1506 | raise HTTPResponseError(res) 1507 | 1508 | def get_config(self): 1509 | """Returns ownCloud config information 1510 | :returns: array of tuples (key, value) for each information 1511 | e.g. [('version', '1.7'), ('website', 'ownCloud'), ('host', 'cloud.example.com'), 1512 | ('contact', ''), ('ssl', 'false')] 1513 | :raises: HTTPResponseError in case an HTTP error status was returned 1514 | """ 1515 | path = 'config' 1516 | res = self._make_ocs_request( 1517 | 'GET', 1518 | '', 1519 | path 1520 | ) 1521 | if res.status_code == 200: 1522 | tree = ET.fromstring(res.content) 1523 | self._check_ocs_status(tree) 1524 | values = [] 1525 | 1526 | element = tree.find('data') 1527 | if element is not None: 1528 | keys = ['version', 'website', 'host', 'contact', 'ssl'] 1529 | for key in keys: 1530 | text = element.find(key).text or '' 1531 | values.append(text) 1532 | return zip(keys, values) 1533 | else: 1534 | return None 1535 | raise HTTPResponseError(res) 1536 | 1537 | def get_attribute(self, app=None, key=None): 1538 | """Returns an application attribute 1539 | 1540 | :param app: application id 1541 | :param key: attribute key or None to retrieve all values for the 1542 | given application 1543 | :returns: attribute value if key was specified, or an array of tuples 1544 | (key, value) for each attribute 1545 | :raises: HTTPResponseError in case an HTTP error status was returned 1546 | """ 1547 | path = 'getattribute' 1548 | if app is not None: 1549 | path += '/' + parse.quote(app, '') 1550 | if key is not None: 1551 | path += '/' + parse.quote(self._encode_string(key), '') 1552 | res = self._make_ocs_request( 1553 | 'GET', 1554 | self.OCS_SERVICE_PRIVATEDATA, 1555 | path 1556 | ) 1557 | if res.status_code == 200: 1558 | tree = ET.fromstring(res.content) 1559 | self._check_ocs_status(tree) 1560 | values = [] 1561 | for element in tree.find('data').iter('element'): 1562 | app_text = element.find('app').text 1563 | key_text = element.find('key').text 1564 | value_text = element.find('value').text or '' 1565 | if key is None: 1566 | if app is None: 1567 | values.append((app_text, key_text, value_text)) 1568 | else: 1569 | values.append((key_text, value_text)) 1570 | else: 1571 | return value_text 1572 | 1573 | if len(values) == 0 and key is not None: 1574 | return None 1575 | return values 1576 | raise HTTPResponseError(res) 1577 | 1578 | def set_attribute(self, app, key, value): 1579 | """Sets an application attribute 1580 | 1581 | :param app: application id 1582 | :param key: key of the attribute to set 1583 | :param value: value to set 1584 | :returns: True if the operation succeeded, False otherwise 1585 | :raises: HTTPResponseError in case an HTTP error status was returned 1586 | """ 1587 | path = 'setattribute/' + parse.quote(app, '') + '/' + parse.quote( 1588 | self._encode_string(key), '') 1589 | res = self._make_ocs_request( 1590 | 'POST', 1591 | self.OCS_SERVICE_PRIVATEDATA, 1592 | path, 1593 | data={'value': self._encode_string(value)} 1594 | ) 1595 | if res.status_code == 200: 1596 | tree = ET.fromstring(res.content) 1597 | self._check_ocs_status(tree) 1598 | return True 1599 | raise HTTPResponseError(res) 1600 | 1601 | def delete_attribute(self, app, key): 1602 | """Deletes an application attribute 1603 | 1604 | :param app: application id 1605 | :param key: key of the attribute to delete 1606 | :returns: True if the operation succeeded, False otherwise 1607 | :raises: HTTPResponseError in case an HTTP error status was returned 1608 | """ 1609 | path = 'deleteattribute/' + parse.quote(app, '') + '/' + parse.quote( 1610 | self._encode_string(key), '') 1611 | res = self._make_ocs_request( 1612 | 'POST', 1613 | self.OCS_SERVICE_PRIVATEDATA, 1614 | path 1615 | ) 1616 | if res.status_code == 200: 1617 | tree = ET.fromstring(res.content) 1618 | self._check_ocs_status(tree) 1619 | return True 1620 | raise HTTPResponseError(res) 1621 | 1622 | def get_apps(self): 1623 | """ List all enabled apps through the provisioning api. 1624 | 1625 | :returns: a dict of apps, with values True/False, representing the enabled state. 1626 | :raises: HTTPResponseError in case an HTTP error status was returned 1627 | """ 1628 | ena_apps = {} 1629 | 1630 | res = self._make_ocs_request('GET', self.OCS_SERVICE_CLOUD, 'apps') 1631 | if res.status_code != 200: 1632 | raise HTTPResponseError(res) 1633 | tree = ET.fromstring(res.content) 1634 | self._check_ocs_status(tree) 1635 | # filesactivity ... 1636 | for el in tree.findall('data/apps/element'): 1637 | ena_apps[el.text] = False 1638 | 1639 | res = self._make_ocs_request('GET', self.OCS_SERVICE_CLOUD, 1640 | 'apps?filter=enabled') 1641 | if res.status_code != 200: 1642 | raise HTTPResponseError(res) 1643 | tree = ET.fromstring(res.content) 1644 | self._check_ocs_status(tree) 1645 | for el in tree.findall('data/apps/element'): 1646 | ena_apps[el.text] = True 1647 | 1648 | return ena_apps 1649 | 1650 | def get_version(self): 1651 | """Gets the ownCloud version of the connected server 1652 | 1653 | :returns: ownCloud version as string 1654 | """ 1655 | if self._version is None: 1656 | self._update_capabilities() 1657 | return self._version 1658 | 1659 | def get_capabilities(self): 1660 | """Gets the ownCloud app capabilities 1661 | 1662 | :returns: capabilities dictionary that maps from 1663 | app name to another dictionary containing the capabilities 1664 | """ 1665 | if self._capabilities is None: 1666 | self._update_capabilities() 1667 | return self._capabilities 1668 | 1669 | def enable_app(self, appname): 1670 | """Enable an app through provisioning_api 1671 | 1672 | :param appname: Name of app to be enabled 1673 | :returns: True if the operation succeeded, False otherwise 1674 | :raises: HTTPResponseError in case an HTTP error status was returned 1675 | 1676 | """ 1677 | res = self._make_ocs_request('POST', self.OCS_SERVICE_CLOUD, 1678 | 'apps/' + appname) 1679 | if res.status_code == 200: 1680 | return True 1681 | 1682 | raise HTTPResponseError(res) 1683 | 1684 | def disable_app(self, appname): 1685 | """Disable an app through provisioning_api 1686 | 1687 | :param appname: Name of app to be disabled 1688 | :returns: True if the operation succeeded, False otherwise 1689 | :raises: HTTPResponseError in case an HTTP error status was returned 1690 | 1691 | """ 1692 | res = self._make_ocs_request('DELETE', self.OCS_SERVICE_CLOUD, 1693 | 'apps/' + appname) 1694 | if res.status_code == 200: 1695 | return True 1696 | 1697 | raise HTTPResponseError(res) 1698 | 1699 | @staticmethod 1700 | def _normalize_path(path): 1701 | """Makes sure the path starts with a "/" 1702 | """ 1703 | if isinstance(path, FileInfo): 1704 | path = path.path 1705 | if len(path) == 0: 1706 | return '/' 1707 | if not path.startswith('/'): 1708 | path = '/' + path 1709 | return path 1710 | 1711 | @staticmethod 1712 | def _encode_string(s): 1713 | """Encodes a unicode instance to utf-8. If a str is passed it will 1714 | simply be returned 1715 | 1716 | :param s: str or unicode to encode 1717 | :returns: encoded output as str 1718 | """ 1719 | if six.PY2 and isinstance(s, unicode): # noqa: F821 1720 | return s.encode('utf-8') 1721 | return s 1722 | 1723 | @staticmethod 1724 | def _check_ocs_status(tree, accepted_codes=[100]): 1725 | """Checks the status code of an OCS request 1726 | 1727 | :param tree: response parsed with elementtree 1728 | :param accepted_codes: list of statuscodes we consider good. E.g. [100,102] can be used to accept a POST 1729 | returning an 'already exists' condition 1730 | :raises: HTTPResponseError if the http status is not 200, or OCSResponseError if the OCS status is not one of the accepted_codes. 1731 | """ 1732 | code_el = tree.find('meta/statuscode') 1733 | if code_el is not None and int(code_el.text) not in accepted_codes: 1734 | r = requests.Response() 1735 | msg_el = tree.find('meta/message') 1736 | if msg_el is None: 1737 | msg_el = tree # fallback to the entire ocs response, if we find no message. 1738 | r._content = ET.tostring(msg_el) 1739 | r.status_code = int(code_el.text) 1740 | raise OCSResponseError(r) 1741 | 1742 | def make_ocs_request(self, method, service, action, **kwargs): 1743 | """Makes a OCS API request and analyses the response 1744 | 1745 | :param method: HTTP method 1746 | :param service: service name 1747 | :param action: action path 1748 | :param \*\*kwargs: optional arguments that ``requests.Request.request`` accepts 1749 | :returns :class:`requests.Response` instance 1750 | """ 1751 | 1752 | accepted_codes = kwargs.pop('accepted_codes', [100]) 1753 | 1754 | res = self._make_ocs_request(method, service, action, **kwargs) 1755 | if res.status_code == 200: 1756 | tree = ET.fromstring(res.content) 1757 | self._check_ocs_status(tree, accepted_codes=accepted_codes) 1758 | return res 1759 | 1760 | raise OCSResponseError(res) 1761 | 1762 | def _make_ocs_request(self, method, service, action, **kwargs): 1763 | """Makes a OCS API request 1764 | 1765 | :param method: HTTP method 1766 | :param service: service name 1767 | :param action: action path 1768 | :param \*\*kwargs: optional arguments that ``requests.Request.request`` accepts 1769 | :returns :class:`requests.Response` instance 1770 | """ 1771 | slash = '' 1772 | if service: 1773 | slash = '/' 1774 | path = self.OCS_BASEPATH + service + slash + action 1775 | 1776 | attributes = kwargs.copy() 1777 | 1778 | if 'headers' not in attributes: 1779 | attributes['headers'] = {} 1780 | 1781 | attributes['headers']['OCS-APIREQUEST'] = 'true' 1782 | 1783 | if self._debug: 1784 | print('OCS request: %s %s %s' % (method, self.url + path, 1785 | attributes)) 1786 | 1787 | res = self._session.request(method, self.url + path, **attributes) 1788 | return res 1789 | 1790 | def _make_dav_request(self, method, path, **kwargs): 1791 | """Makes a WebDAV request 1792 | 1793 | :param method: HTTP method 1794 | :param path: remote path of the targeted file 1795 | :param \*\*kwargs: optional arguments that ``requests.Request.request`` accepts 1796 | :returns array of :class:`FileInfo` if the response 1797 | contains it, or True if the operation succeeded, False 1798 | if it didn't 1799 | """ 1800 | if self._debug: 1801 | print('DAV request: %s %s' % (method, path)) 1802 | if kwargs.get('headers'): 1803 | print('Headers: ', kwargs.get('headers')) 1804 | 1805 | path = self._normalize_path(path) 1806 | res = self._session.request( 1807 | method, 1808 | self._webdav_url + parse.quote(self._encode_string(path)), 1809 | **kwargs 1810 | ) 1811 | if self._debug: 1812 | print('DAV status: %i' % res.status_code) 1813 | if res.status_code in [200, 207]: 1814 | return self._parse_dav_response(res) 1815 | if res.status_code in [204, 201]: 1816 | return True 1817 | raise HTTPResponseError(res) 1818 | 1819 | def _parse_dav_response(self, res): 1820 | """Parses the DAV responses from a multi-status response 1821 | 1822 | :param res: DAV response 1823 | :returns array of :class:`FileInfo` or False if 1824 | the operation did not succeed 1825 | """ 1826 | if res.status_code == 207: 1827 | tree = ET.fromstring(res.content) 1828 | items = [] 1829 | for child in tree: 1830 | items.append(self._parse_dav_element(child)) 1831 | return items 1832 | return False 1833 | 1834 | def _parse_dav_element(self, dav_response): 1835 | """Parses a single DAV element 1836 | 1837 | :param dav_response: DAV response 1838 | :returns :class:`FileInfo` 1839 | """ 1840 | href = parse.unquote( 1841 | self._strip_dav_path(dav_response.find('{DAV:}href').text) 1842 | ) 1843 | 1844 | if six.PY2: 1845 | href = href.decode('utf-8') 1846 | 1847 | file_type = 'file' 1848 | if href[-1] == '/': 1849 | file_type = 'dir' 1850 | 1851 | file_attrs = {} 1852 | attrs = dav_response.find('{DAV:}propstat') 1853 | attrs = attrs.find('{DAV:}prop') 1854 | for attr in attrs: 1855 | file_attrs[attr.tag] = attr.text 1856 | 1857 | return FileInfo(href, file_type, file_attrs) 1858 | 1859 | def _strip_dav_path(self, path): 1860 | """Removes the leading "remote.php/webdav" path from the given path 1861 | 1862 | :param path: path containing the remote DAV path "remote.php/webdav" 1863 | :returns: path stripped of the remote DAV path 1864 | """ 1865 | if path.startswith(self._davpath): 1866 | return path[len(self._davpath):] 1867 | return path 1868 | 1869 | def _webdav_move_copy(self, remote_path_source, remote_path_target, 1870 | operation): 1871 | """Copies or moves a remote file or directory 1872 | 1873 | :param remote_path_source: source file or folder to copy / move 1874 | :param remote_path_target: target file to which to copy / move 1875 | :param operation: MOVE or COPY 1876 | 1877 | :returns: True if the operation succeeded, False otherwise 1878 | :raises: HTTPResponseError in case an HTTP error status was returned 1879 | """ 1880 | 1881 | if operation != "MOVE" and operation != "COPY": 1882 | return False 1883 | 1884 | if remote_path_target[-1] == '/': 1885 | remote_path_target += os.path.basename(remote_path_source) 1886 | 1887 | if not (remote_path_target[0] == '/'): 1888 | remote_path_target = '/' + remote_path_target 1889 | 1890 | remote_path_source = self._normalize_path(remote_path_source) 1891 | headers = { 1892 | 'Destination': self._webdav_url + parse.quote( 1893 | self._encode_string(remote_path_target)) 1894 | } 1895 | 1896 | return self._make_dav_request( 1897 | operation, 1898 | remote_path_source, 1899 | headers=headers 1900 | ) 1901 | 1902 | def _xml_to_dict(self, element): 1903 | """ 1904 | Take an XML element, iterate over it and build a dict 1905 | 1906 | :param element: An xml.etree.ElementTree.Element, or a list of the same 1907 | :returns: A dictionary 1908 | """ 1909 | return_dict = {} 1910 | for el in element: 1911 | return_dict[el.tag] = None 1912 | children = list(el) 1913 | if children: 1914 | return_dict[el.tag] = self._xml_to_dict(children) 1915 | else: 1916 | return_dict[el.tag] = el.text 1917 | return return_dict 1918 | 1919 | def _get_shareinfo(self, data_el): 1920 | """Simple helper which returns instance of ShareInfo class 1921 | 1922 | :param data_el: 'data' element extracted from _make_ocs_request 1923 | :returns: instance of ShareInfo class 1924 | """ 1925 | if (data_el is None) or not (isinstance(data_el, ET.Element)): 1926 | return None 1927 | return ShareInfo(self._xml_to_dict(data_el)) 1928 | 1929 | def _update_capabilities(self): 1930 | res = self._make_ocs_request( 1931 | 'GET', 1932 | self.OCS_SERVICE_CLOUD, 1933 | 'capabilities' 1934 | ) 1935 | if res.status_code == 200: 1936 | tree = ET.fromstring(res.content) 1937 | self._check_ocs_status(tree) 1938 | 1939 | data_el = tree.find('data') 1940 | apps = {} 1941 | for app_el in data_el.find('capabilities'): 1942 | app_caps = {} 1943 | for cap_el in app_el: 1944 | app_caps[cap_el.tag] = cap_el.text 1945 | apps[app_el.tag] = app_caps 1946 | 1947 | self._capabilities = apps 1948 | 1949 | version_el = data_el.find('version/string') 1950 | edition_el = data_el.find('version/edition') 1951 | self._version = version_el.text 1952 | if edition_el.text is not None: 1953 | self._version += '-' + edition_el.text 1954 | 1955 | if 'dav' in apps and 'chunking' in apps['dav']: 1956 | chunking_version = float(apps['dav']['chunking']) 1957 | if self._dav_endpoint_version > chunking_version: 1958 | self._dav_endpoint_version = None 1959 | 1960 | if self._dav_endpoint_version is None and chunking_version >= 1.0: 1961 | self._dav_endpoint_version = 1 1962 | else: 1963 | self._dav_endpoint_version = 0 1964 | 1965 | return self._capabilities 1966 | raise HTTPResponseError(res) 1967 | -------------------------------------------------------------------------------- /owncloud/test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/owncloud/pyocclient/1eec85496cd547271f37796e2fba75eaac725185/owncloud/test/__init__.py -------------------------------------------------------------------------------- /owncloud/test/config.py.sample: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # vim: expandtab shiftwidth=4 softtabstop=4 3 | # 4 | import time 5 | 6 | # unique id to use for testing 7 | test_id = int(time.time()) 8 | 9 | Config = { 10 | # Change this to your ownCloud's URL 11 | 'owncloud_url': 'http://127.0.0.1/', 12 | # ownCloud login 13 | 'owncloud_login': 'admin', 14 | # ownCloud password 15 | 'owncloud_password': 'admin', 16 | # test user whom we want to share a file 17 | 'owncloud_share2user': 'share', 18 | # test group, uses to add owncloud_share2user to it etc 19 | 'test_group': 'my_test_group', 20 | # remote root path to use for testing 21 | 'test_root': 'pyoctestroot%s' % test_id, 22 | # app name to use when testing privatedata API 23 | 'app_name': 'pyocclient_test%s' % test_id, 24 | #groups to be created 25 | 'groups_to_create': ["grp1","grp2","grp3"], 26 | #not existing group 27 | 'not_existing_group': 'this_group_should_not_exist' 28 | } 29 | -------------------------------------------------------------------------------- /owncloud/test/test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # vim: expandtab shiftwidth=4 softtabstop=4 3 | # 4 | import unittest 5 | from unittest_data_provider import data_provider 6 | import os 7 | import shutil 8 | import owncloud 9 | import datetime 10 | import time 11 | import tempfile 12 | import random 13 | import six 14 | 15 | from config import Config 16 | 17 | def getSupportedDavVersion(): 18 | # connect just to check supported DAV version 19 | client = owncloud.Client(Config['owncloud_url']) 20 | client.login(Config['owncloud_login'], Config['owncloud_password']) 21 | 22 | caps = client.get_capabilities() 23 | dav_version = None 24 | 25 | if 'dav' in caps and 'chunking' in caps['dav']: 26 | dav_version = float(caps['dav']['chunking']) 27 | 28 | return dav_version 29 | 30 | def skipIfDavVersionLessThan(expected_version): 31 | dav_version = getSupportedDavVersion() 32 | if dav_version is None or dav_version < expected_version: 33 | return unittest.skip("Expected DAV version %s not supported in this ownCloud version" % expected_version) 34 | return lambda func: func 35 | 36 | class TestFileAccess(unittest.TestCase): 37 | 38 | def get_dav_endpoint_version(self): 39 | return 0 40 | 41 | def files(): 42 | return ( 43 | ['test.txt'], 44 | ['test space and + and #.txt'], 45 | [u'文件.txt'] 46 | ) 47 | 48 | def files_content(): 49 | return ( 50 | ['test.txt', b'Hello world!', 'subdir'], 51 | ['test space and + and #.txt', b'Hello space with+plus#hash!', 'subdir with space + plus and #hash'], 52 | [u'文件.txt', u'你好世界'.encode('utf-8'), u'文件夹'] 53 | ) 54 | 55 | def setUp(self): 56 | self.temp_dir = tempfile.gettempdir() + '/pyocclient_test%s-%s/' % (int(time.time()), random.randint(1, 1000)) 57 | os.mkdir(self.temp_dir) 58 | 59 | self.client = owncloud.Client(Config['owncloud_url'], dav_endpoint_version=self.get_dav_endpoint_version()) 60 | self.client.login(Config['owncloud_login'], Config['owncloud_password']) 61 | self.test_root = Config['test_root'] 62 | if not self.test_root[-1] == '/': 63 | self.test_root += '/' 64 | if not self.test_root[0] == '/': 65 | self.test_root = '/' + self.test_root 66 | self.client.mkdir(self.test_root) 67 | self.share2user = Config['owncloud_share2user'] 68 | self.share2userPwd = 'Avcpwd4l!' 69 | self.test_group = Config['test_group'] 70 | try: 71 | self.client.create_user(self.share2user, self.share2userPwd) 72 | except: 73 | pass 74 | try: 75 | self.client.create_group(self.test_group) 76 | except: 77 | pass 78 | 79 | def tearDown(self): 80 | self.client.delete(self.test_root) 81 | try: 82 | self.client.delete_user(self.share2user) 83 | except: 84 | pass 85 | try: 86 | self.client.delete_group(self.test_group) 87 | except: 88 | pass 89 | self.client.logout() 90 | shutil.rmtree(self.temp_dir) 91 | 92 | @staticmethod 93 | def __create_file(target_file, size): 94 | file_handle = open(target_file, 'w') 95 | dummy_data = '' 96 | for i in range(0, 1024): 97 | dummy_data += 'X' 98 | 99 | for i in range(0, int(size / 1024)): 100 | # write in 1kb blocks 101 | file_handle.write(dummy_data) 102 | 103 | dummy_data = '' 104 | for i in range(0, size % 1024): 105 | dummy_data += 'X' 106 | 107 | if (len(dummy_data) > 0): 108 | file_handle.write(dummy_data) 109 | 110 | file_handle.close() 111 | 112 | @data_provider(files_content) 113 | def test_mkdir(self, file_name, content, subdir): 114 | """Test subdirectory creation""" 115 | self.assertTrue(self.client.mkdir(self.test_root + subdir)) 116 | self.assertIsNotNone(self.client.file_info(self.test_root + subdir)) 117 | 118 | @data_provider(files_content) 119 | def test_put_file_contents(self, file_name, content, subdir): 120 | """Test creating remote file with given contents""" 121 | self.assertTrue(self.client.mkdir(self.test_root + subdir)) 122 | self.assertTrue(self.client.put_file_contents(self.test_root + subdir + '/' + file_name, content)) 123 | 124 | @data_provider(files_content) 125 | def test_get_file_contents(self, file_name, content, subdir): 126 | """Test reading remote file""" 127 | self.assertTrue(self.client.mkdir(self.test_root + subdir)) 128 | self.assertTrue(self.client.put_file_contents(self.test_root + subdir + '/' + file_name, content)) 129 | self.assertEqual(self.client.get_file_contents(self.test_root + subdir + '/' + file_name), content) 130 | 131 | @data_provider(files_content) 132 | def test_get_file_info(self, file_name, content, subdir): 133 | """Test getting file info""" 134 | self.assertTrue(self.client.mkdir(self.test_root + subdir)) 135 | self.assertTrue(self.client.put_file_contents(self.test_root + file_name, content)) 136 | 137 | file_info = self.client.file_info(self.test_root + file_name) 138 | self.assertTrue(isinstance(file_info, owncloud.FileInfo)) 139 | self.assertEqual(file_info.get_name(), file_name) 140 | self.assertEqual(file_info.get_path() + '/', self.test_root) 141 | self.assertEqual(file_info.get_size(), len(content)) 142 | self.assertIsNotNone(file_info.get_etag()) 143 | self.assertEqual(file_info.get_content_type(), 'text/plain') 144 | self.assertTrue(type(file_info.get_last_modified()) is datetime.datetime) 145 | self.assertFalse(file_info.is_dir()) 146 | 147 | dir_info = self.client.file_info(self.test_root + subdir) 148 | self.assertTrue(isinstance(dir_info, owncloud.FileInfo)) 149 | self.assertEqual(dir_info.get_name(), subdir) 150 | self.assertEqual(file_info.get_path() + '/', self.test_root) 151 | self.assertIsNone(dir_info.get_size()) 152 | self.assertIsNotNone(dir_info.get_etag()) 153 | self.assertEqual(dir_info.get_content_type(), 'httpd/unix-directory') 154 | self.assertTrue(type(dir_info.get_last_modified()) is datetime.datetime) 155 | self.assertTrue(dir_info.is_dir()) 156 | 157 | @data_provider(files_content) 158 | def test_get_file_info_with_properties(self, file_name, content, subdir): 159 | """Test getting file info with extra properties""" 160 | self.assertTrue(self.client.mkdir(self.test_root + subdir)) 161 | self.assertTrue(self.client.put_file_contents(self.test_root + file_name, content)) 162 | 163 | # ensure we first pass all the properties expected by the tests below, then any extra attributes 164 | file_info = self.client.file_info(self.test_root + file_name, [ 165 | 'd:getlastmodified', 166 | 'd:getcontentlength', 167 | 'd:resourcetype', 168 | 'd:getetag', 169 | 'd:getcontenttype', 170 | 'oc:favorite', 171 | 'oc:owner-id', 172 | 'oc:owner-display-name' 173 | ]) 174 | self.assertTrue(isinstance(file_info, owncloud.FileInfo)) 175 | self.assertEqual(file_info.get_name(), file_name) 176 | self.assertEqual(file_info.get_path() + '/', self.test_root) 177 | self.assertEqual(file_info.get_size(), len(content)) 178 | self.assertIsNotNone(file_info.get_etag()) 179 | self.assertEqual(file_info.get_content_type(), 'text/plain') 180 | self.assertTrue(type(file_info.get_last_modified()) is datetime.datetime) 181 | self.assertFalse(file_info.is_dir()) 182 | self.assertIsNotNone(file_info.attributes['{http://owncloud.org/ns}favorite']) 183 | self.assertIsNotNone(file_info.attributes['{http://owncloud.org/ns}owner-id']) 184 | self.assertIsNotNone(file_info.attributes['{http://owncloud.org/ns}owner-display-name']) 185 | 186 | dir_info = self.client.file_info(self.test_root + subdir, [ 187 | 'd:getlastmodified', 188 | 'd:getcontentlength', 189 | 'd:resourcetype', 190 | 'd:getetag', 191 | 'd:getcontenttype', 192 | 'oc:favorite', 193 | 'oc:owner-id', 194 | 'oc:owner-display-name' 195 | ]) 196 | self.assertTrue(isinstance(dir_info, owncloud.FileInfo)) 197 | self.assertEqual(dir_info.get_name(), subdir) 198 | self.assertEqual(file_info.get_path() + '/', self.test_root) 199 | self.assertIsNone(dir_info.get_size()) 200 | self.assertIsNotNone(dir_info.get_etag()) 201 | self.assertEqual(dir_info.get_content_type(), 'httpd/unix-directory') 202 | self.assertTrue(type(dir_info.get_last_modified()) is datetime.datetime) 203 | self.assertTrue(dir_info.is_dir()) 204 | self.assertIsNotNone(dir_info.attributes['{http://owncloud.org/ns}favorite']) 205 | self.assertIsNotNone(dir_info.attributes['{http://owncloud.org/ns}owner-id']) 206 | self.assertIsNotNone(dir_info.attributes['{http://owncloud.org/ns}owner-display-name']) 207 | 208 | def test_get_file_info_non_existing(self): 209 | """Test getting file info for non existing file""" 210 | with self.assertRaises(owncloud.ResponseError) as e: 211 | self.client.file_info(self.test_root + 'unexist') 212 | self.assertEqual(e.exception.status_code, 404) 213 | 214 | def test_get_file_listing(self): 215 | """Test getting file listing""" 216 | self.assertTrue(self.client.put_file_contents(self.test_root + 'file one.txt', 'first file')) 217 | self.assertTrue(self.client.put_file_contents(self.test_root + 'zz+z.txt', 'z file')) 218 | self.assertTrue(self.client.put_file_contents(self.test_root + u'中文.txt', '')) 219 | self.assertTrue(self.client.put_file_contents(self.test_root + 'abc.txt', '')) 220 | self.assertTrue(self.client.mkdir(self.test_root + 'subdir')) 221 | self.assertTrue(self.client.put_file_contents(self.test_root + 'subdir/in dir.txt', '')) 222 | 223 | listing = self.client.list(self.test_root) 224 | self.assertEqual(len(listing), 5) 225 | self.assertEqual(listing[0].get_name(), 'abc.txt') 226 | self.assertEqual(listing[1].get_name(), 'file one.txt') 227 | self.assertEqual(listing[2].get_name(), 'subdir') 228 | self.assertEqual(listing[3].get_name(), 'zz+z.txt') 229 | self.assertEqual(listing[4].get_name(), u'中文.txt') 230 | 231 | self.assertTrue(listing[2].is_dir()) 232 | self.assertFalse(listing[3].is_dir()) 233 | 234 | listing = self.client.list(self.test_root, depth="infinity") 235 | self.assertEqual(len(listing), 6) 236 | self.assertEqual(listing[2].get_name(), 'subdir') 237 | self.assertEqual(listing[3].get_name(), 'in dir.txt') 238 | 239 | listing = self.client.list(self.test_root, depth=2) 240 | self.assertEqual(len(listing), 6) 241 | self.assertEqual(listing[2].get_name(), 'subdir') 242 | self.assertEqual(listing[3].get_name(), 'in dir.txt') 243 | 244 | def test_get_file_listing_with_properties(self): 245 | """Test getting file listing with extra properties""" 246 | self.assertTrue(self.client.put_file_contents(self.test_root + 'file one.txt', 'first file')) 247 | self.assertTrue(self.client.put_file_contents(self.test_root + 'zz+z.txt', 'z file')) 248 | self.assertTrue(self.client.put_file_contents(self.test_root + u'中文.txt', '')) 249 | self.assertTrue(self.client.put_file_contents(self.test_root + 'abc.txt', '')) 250 | self.assertTrue(self.client.mkdir(self.test_root + 'subdir')) 251 | self.assertTrue(self.client.put_file_contents(self.test_root + 'subdir/in dir.txt', '')) 252 | 253 | listing = self.client.list(self.test_root, depth=1, properties=[ 254 | 'd:getlastmodified', 255 | 'd:getcontentlength', 256 | 'd:resourcetype', 257 | 'd:getetag', 258 | 'd:getcontenttype', 259 | 'oc:favorite', 260 | 'oc:owner-id', 261 | 'oc:owner-display-name' 262 | ]) 263 | self.assertEqual(len(listing), 5) 264 | self.assertEqual(listing[0].get_name(), 'abc.txt') 265 | self.assertEqual(listing[1].get_name(), 'file one.txt') 266 | self.assertEqual(listing[2].get_name(), 'subdir') 267 | self.assertEqual(listing[3].get_name(), 'zz+z.txt') 268 | self.assertEqual(listing[4].get_name(), u'中文.txt') 269 | 270 | self.assertTrue(listing[2].is_dir()) 271 | self.assertFalse(listing[3].is_dir()) 272 | 273 | for i in range(5): 274 | self.assertIsNotNone(listing[i].attributes['{http://owncloud.org/ns}favorite']) 275 | self.assertIsNotNone(listing[i].attributes['{http://owncloud.org/ns}owner-id']) 276 | self.assertIsNotNone(listing[i].attributes['{http://owncloud.org/ns}owner-display-name']) 277 | 278 | 279 | def test_get_file_listing_non_existing(self): 280 | """Test getting file listing for non existing directory""" 281 | with self.assertRaises(owncloud.ResponseError) as e: 282 | self.client.list(self.test_root + 'unexist') 283 | self.assertEqual(e.exception.status_code, 404) 284 | 285 | @data_provider(files) 286 | def test_upload_small_file(self, file_name): 287 | """Test simple upload""" 288 | temp_file = self.temp_dir + 'pyoctest.dat' 289 | self.__create_file(temp_file, 2 * 1024) 290 | self.assertTrue(self.client.put_file(self.test_root + file_name, temp_file, chunked=False)) 291 | os.unlink(temp_file) 292 | 293 | file_info = self.client.file_info(self.test_root + file_name) 294 | self.assertIsNotNone(file_info) 295 | self.assertEqual(file_info.get_size(), 2 * 1024) 296 | 297 | def test_upload_two_chunks(self): 298 | """Test chunked upload with two chunks""" 299 | temp_file = self.temp_dir + 'pyoctest.dat' 300 | self.__create_file(temp_file, 18 * 1024 * 1024 + 1) 301 | self.assertTrue(self.client.put_file(self.test_root + 'chunk_test.dat', temp_file)) 302 | os.unlink(temp_file) 303 | 304 | file_info = self.client.file_info(self.test_root + 'chunk_test.dat') 305 | 306 | self.assertIsNotNone(file_info) 307 | self.assertEqual(file_info.get_size(), 18 * 1024 * 1024 + 1) 308 | 309 | def test_upload_chunks_minus_one_byte(self): 310 | """Test chunked upload minus one byte""" 311 | temp_file = self.temp_dir + 'pyoctest.dat' 312 | self.__create_file(temp_file, 2 * 1024 - 1) 313 | self.assertTrue(self.client.put_file(self.test_root + 'chunk_test.dat', temp_file, chunk_size = 1024)) 314 | os.unlink(temp_file) 315 | 316 | file_info = self.client.file_info(self.test_root + 'chunk_test.dat') 317 | 318 | self.assertIsNotNone(file_info) 319 | self.assertEqual(file_info.get_size(), 2 * 1024 - 1) 320 | 321 | @data_provider(files) 322 | def test_upload_big_file(self, file_name): 323 | """Test chunked upload""" 324 | temp_file = self.temp_dir + 'pyoctest.dat' 325 | self.__create_file(temp_file, 22 * 1024 * 1024) 326 | self.assertTrue(self.client.put_file(self.test_root + file_name, temp_file)) 327 | os.unlink(temp_file) 328 | 329 | file_info = self.client.file_info(self.test_root + file_name) 330 | self.assertIsNotNone(file_info) 331 | self.assertEqual(file_info.get_size(), 22 * 1024 * 1024) 332 | 333 | def test_upload_timestamp(self): 334 | # TODO: test with keeping timestamp and not keeping it 335 | pass 336 | 337 | def test_upload_directory(self): 338 | temp_dir = self.temp_dir + 'pyoctest.dir/' 339 | os.mkdir(temp_dir) 340 | os.mkdir(temp_dir + 'levelone') 341 | os.mkdir(temp_dir + 'levelone/leveltwo') 342 | 343 | self.__create_file(temp_dir + 'file1.dat', 2 * 1024) 344 | self.__create_file(temp_dir + 'file2.dat', 22 * 1024 * 1024) 345 | self.__create_file(temp_dir + 'levelone/file3.dat', 22 * 1024 * 1024) 346 | self.__create_file(temp_dir + 'levelone/leveltwo/file4.dat', 8 * 1024 * 1024) 347 | self.__create_file(temp_dir + u'levelone/文件.dat', 7 * 1024 * 1024) 348 | 349 | self.assertTrue(self.client.mkdir(self.test_root + 'subdir')) 350 | self.assertTrue(self.client.put_directory(self.test_root + 'subdir', temp_dir)) 351 | 352 | self.assertIsNotNone(self.client.file_info(self.test_root + 'subdir/pyoctest.dir')) 353 | self.assertIsNotNone(self.client.file_info(self.test_root + 'subdir/pyoctest.dir/file1.dat')) 354 | self.assertIsNotNone(self.client.file_info(self.test_root + 'subdir/pyoctest.dir/file2.dat')) 355 | self.assertIsNotNone(self.client.file_info(self.test_root + 'subdir/pyoctest.dir/levelone/file3.dat')) 356 | self.assertIsNotNone(self.client.file_info(self.test_root + 'subdir/pyoctest.dir/levelone/leveltwo/file4.dat')) 357 | self.assertIsNotNone(self.client.file_info(self.test_root + u'subdir/pyoctest.dir/levelone/文件.dat')) 358 | 359 | @data_provider(files_content) 360 | def test_download_file(self, file_name, content, subdir): 361 | """Test file download""" 362 | temp_file = self.temp_dir + 'pyoctest.dat' 363 | self.assertTrue(self.client.mkdir(self.test_root + subdir)) 364 | self.assertTrue(self.client.put_file_contents(self.test_root + subdir + '/' + file_name, content)) 365 | 366 | self.assertTrue(self.client.get_file(self.test_root + subdir + '/' + file_name, temp_file)) 367 | 368 | f = open(temp_file, 'rb') 369 | s = f.read() 370 | f.close() 371 | os.unlink(temp_file) 372 | self.assertEqual(s, content) 373 | 374 | def test_download_dir(self): 375 | import zipfile 376 | """Test directory download as zip""" 377 | self.assertTrue(self.client.mkdir(self.test_root + 'subdir')) 378 | self.assertTrue(self.client.put_file_contents(self.test_root + 'subdir/test.txt', 'hello world!')) 379 | # Note: this can only work properly with OC 7 380 | #self.assertTrue(self.client.put_file_contents(self.test_root + 'subdir/文件.txt', '你好世界!')) 381 | 382 | temp_file = self.temp_dir + 'pyoctest.zip' 383 | self.assertTrue(self.client.get_directory_as_zip(self.test_root, temp_file)) 384 | 385 | self.assertTrue(os.stat(temp_file)) 386 | 387 | zip_info = zipfile.ZipFile(temp_file) 388 | listing = zip_info.namelist() 389 | 390 | self.assertEqual(len(listing), 3) 391 | os.unlink(temp_file) 392 | 393 | @data_provider(files_content) 394 | def test_delete_file(self, file_name, content, subdir): 395 | """Test file deletion""" 396 | self.assertTrue(self.client.mkdir(self.test_root + subdir)) 397 | self.assertTrue(self.client.put_file_contents(self.test_root + subdir + '/' + file_name, content)) 398 | self.assertTrue(self.client.delete(self.test_root + subdir + '/' + file_name)) 399 | with self.assertRaises(owncloud.ResponseError) as e: 400 | self.client.file_info(self.test_root + subdir + '/' + file_name) 401 | self.assertEqual(e.exception.status_code, 404) 402 | 403 | @data_provider(files_content) 404 | def test_delete_dir(self, file_name, content, subdir): 405 | """Test directory deletion""" 406 | self.assertTrue(self.client.mkdir(self.test_root + subdir)) 407 | self.assertTrue(self.client.put_file_contents(self.test_root + subdir + '/' + file_name, content)) 408 | self.assertTrue(self.client.delete(self.test_root + subdir)) 409 | with self.assertRaises(owncloud.ResponseError) as e: 410 | self.client.file_info(self.test_root + subdir + '/' + file_name) 411 | self.assertEqual(e.exception.status_code, 404) 412 | with self.assertRaises(owncloud.ResponseError) as e: 413 | self.client.file_info(self.test_root + subdir) 414 | self.assertEqual(e.exception.status_code, 404) 415 | 416 | def test_move_rename_in_place(self): 417 | """Test rename in place""" 418 | 419 | self.assertTrue( 420 | self.client.put_file_contents( 421 | self.test_root + 'rename this file!.txt', 422 | 'to rename' 423 | ) 424 | ) 425 | self.assertTrue( 426 | self.client.move( 427 | self.test_root + 'rename this file!.txt', 428 | self.test_root + 'renamed in place.txt' 429 | ) 430 | ) 431 | self.assertEqual( 432 | self.client.get_file_contents( 433 | self.test_root + 'renamed in place.txt' 434 | ), 435 | b'to rename' 436 | ) 437 | 438 | def test_move_and_rename(self): 439 | """Test rename into subdir""" 440 | 441 | self.assertTrue( 442 | self.client.put_file_contents( 443 | self.test_root + 'move and rename.txt', 444 | 'first file' 445 | ) 446 | ) 447 | self.assertTrue( 448 | self.client.mkdir( 449 | self.test_root + 'subdir' 450 | ) 451 | ) 452 | self.assertTrue( 453 | self.client.move( 454 | self.test_root + 'move and rename.txt', 455 | self.test_root + 'subdir/file renamed.txt' 456 | ) 457 | ) 458 | self.assertEqual( 459 | self.client.get_file_contents( 460 | self.test_root + 'subdir/file renamed.txt' 461 | ), 462 | b'first file' 463 | ) 464 | 465 | def test_move_to_dir(self): 466 | """Test move into directory""" 467 | 468 | self.assertTrue( 469 | self.client.put_file_contents( 470 | self.test_root + 'movetodir.txt', 471 | 'z file' 472 | ) 473 | ) 474 | # move to subdir 475 | self.assertTrue( 476 | self.client.mkdir( 477 | self.test_root + 'subdir' 478 | ) 479 | ) 480 | self.assertTrue( 481 | self.client.move( 482 | self.test_root + 'movetodir.txt', 483 | self.test_root + 'subdir/' 484 | ) 485 | ) 486 | self.assertEqual( 487 | self.client.get_file_contents( 488 | self.test_root + 'subdir/movetodir.txt' 489 | ), 490 | b'z file' 491 | ) 492 | 493 | def test_move_subdir(self): 494 | """Test move subdir""" 495 | 496 | # subdir to move 497 | self.assertTrue( 498 | self.client.mkdir( 499 | self.test_root + 'subdir_to_move' 500 | ) 501 | ) 502 | self.assertTrue( 503 | self.client.put_file_contents( 504 | self.test_root + 'subdir_to_move/file two.txt', 505 | 'second file' 506 | ) 507 | ) 508 | self.assertTrue( 509 | self.client.mkdir( 510 | self.test_root + 'subdir' 511 | ) 512 | ) 513 | self.assertTrue( 514 | self.client.move( 515 | self.test_root + 'subdir_to_move', 516 | self.test_root + 'subdir/' 517 | ) 518 | ) 519 | 520 | def test_rename_unicode(self): 521 | """Test rename unicode""" 522 | 523 | # rename 524 | self.assertTrue( 525 | self.client.put_file_contents( 526 | self.test_root + u'中文.txt', 527 | '1' 528 | ) 529 | ) 530 | self.assertTrue( 531 | self.client.move( 532 | self.test_root + u'中文.txt', 533 | self.test_root + u'更多中文.txt' 534 | ) 535 | ) 536 | self.assertEqual( 537 | self.client.get_file_contents( 538 | self.test_root + u'更多中文.txt' 539 | ), 540 | b'1' 541 | ) 542 | 543 | def test_move_unicode(self): 544 | """Test move unicode to dir""" 545 | self.assertTrue( 546 | self.client.put_file_contents( 547 | self.test_root + u'中文.txt', 548 | '2' 549 | ) 550 | ) 551 | self.assertTrue( 552 | self.client.mkdir( 553 | self.test_root + 'subdir' 554 | ) 555 | ) 556 | self.assertTrue( 557 | self.client.move( 558 | self.test_root + u'中文.txt', 559 | self.test_root + u'subdir/中文.txt' 560 | ) 561 | ) 562 | self.assertEqual( 563 | self.client.get_file_contents( 564 | self.test_root + u'subdir/中文.txt' 565 | ), 566 | b'2' 567 | ) 568 | 569 | def test_move_to_non_existing_dir(self): 570 | """Test error when moving to non existing dir""" 571 | 572 | self.assertTrue( 573 | self.client.put_file_contents( 574 | self.test_root + 'move not possible.txt', 575 | 'x' 576 | ) 577 | ) 578 | with self.assertRaises(owncloud.ResponseError) as e: 579 | self.client.move( 580 | self.test_root + 'move not possible.txt', 581 | self.test_root + 'non-existing-dir/subdir/x.txt' 582 | ) 583 | self.assertEqual(e.exception.status_code, 409) 584 | 585 | self.assertEqual( 586 | self.client.get_file_contents( 587 | self.test_root + 'move not possible.txt' 588 | ), 589 | b'x' 590 | ) 591 | 592 | def test_copy_in_place(self): 593 | """Test copy in place""" 594 | 595 | self.assertTrue( 596 | self.client.put_file_contents( 597 | self.test_root + 'copy this file!.txt', 598 | 'to copy' 599 | ) 600 | ) 601 | self.assertTrue( 602 | self.client.copy( 603 | self.test_root + 'copy this file!.txt', 604 | self.test_root + 'copied in place.txt' 605 | ) 606 | ) 607 | self.assertEqual( 608 | self.client.get_file_contents( 609 | self.test_root + 'copied in place.txt' 610 | ), 611 | b'to copy' 612 | ) 613 | 614 | self.assertEqual( 615 | self.client.get_file_contents( 616 | self.test_root + 'copy this file!.txt' 617 | ), 618 | b'to copy' 619 | ) 620 | 621 | def test_copy_into_subdir(self): 622 | """Test copy into subdir""" 623 | 624 | self.assertTrue( 625 | self.client.put_file_contents( 626 | self.test_root + 'copy into subdir.txt', 627 | 'first file' 628 | ) 629 | ) 630 | self.assertTrue( 631 | self.client.mkdir( 632 | self.test_root + 'subdir' 633 | ) 634 | ) 635 | self.assertTrue( 636 | self.client.copy( 637 | self.test_root + 'copy into subdir.txt', 638 | self.test_root + 'subdir/file copied.txt' 639 | ) 640 | ) 641 | self.assertEqual( 642 | self.client.get_file_contents( 643 | self.test_root + 'subdir/file copied.txt' 644 | ), 645 | b'first file' 646 | ) 647 | self.assertEqual( 648 | self.client.get_file_contents( 649 | self.test_root + 'copy into subdir.txt' 650 | ), 651 | b'first file' 652 | ) 653 | 654 | def test_copy_unicode(self): 655 | """Test copy unicode to dir""" 656 | self.assertTrue( 657 | self.client.put_file_contents( 658 | self.test_root + u'दिलै होस छोरा होस.txt', 659 | 'content' 660 | ) 661 | ) 662 | self.assertTrue( 663 | self.client.mkdir( 664 | self.test_root + 'subdir' 665 | ) 666 | ) 667 | self.assertTrue( 668 | self.client.copy( 669 | self.test_root + u'दिलै होस छोरा होस.txt', 670 | self.test_root + u'subdir/दिलै होस छोरा होस.txt' 671 | ) 672 | ) 673 | self.assertEqual( 674 | self.client.get_file_contents( 675 | self.test_root + u'subdir/दिलै होस छोरा होस.txt' 676 | ), 677 | b'content' 678 | ) 679 | self.assertEqual( 680 | self.client.get_file_contents( 681 | self.test_root + u'दिलै होस छोरा होस.txt' 682 | ), 683 | b'content' 684 | ) 685 | 686 | def test_copy_to_non_existing_dir(self): 687 | """Test error when copy to non existing dir""" 688 | 689 | self.assertTrue( 690 | self.client.put_file_contents( 691 | self.test_root + 'copy not possible.txt', 692 | 'x' 693 | ) 694 | ) 695 | with self.assertRaises(owncloud.ResponseError) as e: 696 | self.client.copy( 697 | self.test_root + 'copy not possible.txt', 698 | self.test_root + 'non-existing-dir/subdir/x.txt' 699 | ) 700 | self.assertEqual(e.exception.status_code, 409) 701 | 702 | self.assertEqual( 703 | self.client.get_file_contents( 704 | self.test_root + 'copy not possible.txt' 705 | ), 706 | b'x' 707 | ) 708 | 709 | @data_provider(files) 710 | def test_share_with_link(self, file_name): 711 | """Test sharing a file with link""" 712 | 713 | path = self.test_root + file_name 714 | self.assertTrue(self.client.put_file_contents(path, 'hello world!')) 715 | 716 | share_info = self.client.share_file_with_link(path, public_upload=False, password='AnvvsP1234', name='Test Link') 717 | 718 | self.assertTrue(self.client.is_shared(path)) 719 | self.assertTrue(isinstance(share_info, owncloud.ShareInfo)) 720 | self.assertTrue(type(share_info.get_id()) is int) 721 | self.assertEqual(share_info.get_path(), path) 722 | self.assertEqual(share_info.get_name(), 'Test Link') 723 | self.assertTrue(type(share_info.get_link()) is str) 724 | self.assertTrue(type(share_info.get_token()) is str) 725 | 726 | def test_share_with_link_non_existing_file(self): 727 | """Test sharing a file with link""" 728 | with self.assertRaises(owncloud.ResponseError) as e: 729 | self.client.share_file_with_link(self.test_root + 'unexist.txt') 730 | self.assertEqual(e.exception.status_code, 404) 731 | 732 | @data_provider(files) 733 | def test_share_with_user(self, file_name): 734 | """Test sharing a file to user""" 735 | 736 | path = self.test_root + file_name 737 | self.assertTrue(self.client.put_file_contents(path, 'hello world!')) 738 | 739 | share_info = self.client.share_file_with_user(path, self.share2user) 740 | 741 | self.assertTrue(self.client.is_shared(path)) 742 | self.assertTrue(isinstance(share_info, owncloud.ShareInfo)) 743 | self.assertEqual(share_info.get_path(), path) 744 | self.assertTrue(type(share_info.get_id()) is int) 745 | self.assertEqual(share_info.get_permissions(), 1) 746 | 747 | shareclient = owncloud.Client(Config['owncloud_url']) 748 | shareclient.login(self.share2user, self.share2userPwd) 749 | share2_info = shareclient.get_shares( 750 | "/", shared_with_me=True)[0].share_info 751 | self.assertEqual(share2_info["uid_owner"], Config['owncloud_login']) 752 | self.assertEqual(file_name, share2_info["file_target"][1:]) 753 | shareclient.logout() 754 | 755 | self.assertTrue(self.client.delete(path)) 756 | 757 | @data_provider(files) 758 | def test_share_with_group(self, file_name): 759 | """Test sharing a file to a group""" 760 | 761 | path = self.test_root + file_name 762 | self.assertTrue(self.client.put_file_contents(path, 'hello world!')) 763 | 764 | share_info = self.client.share_file_with_group(path, self.test_group, perms=31) 765 | 766 | self.assertTrue(self.client.is_shared(path)) 767 | self.assertTrue(isinstance(share_info, owncloud.ShareInfo)) 768 | self.assertEqual(share_info.get_path(), path) 769 | self.assertTrue(type(share_info.get_id()) is int) 770 | self.assertEqual(share_info.get_permissions(), 31) 771 | self.assertTrue(self.client.delete(path)) 772 | 773 | @data_provider(files) 774 | def test_delete_share(self, file_name): 775 | """Test deleting a share (by id)""" 776 | 777 | path = self.test_root + file_name 778 | self.assertTrue(self.client.put_file_contents(path, 'hello world!')) 779 | 780 | share_info = self.client.share_file_with_user(path, self.share2user) 781 | 782 | self.assertTrue(self.client.is_shared(path)) 783 | self.assertIsNotNone(self.client.delete_share(share_info.get_id())) 784 | self.assertFalse(self.client.is_shared(path)) 785 | 786 | def test_is_shared_non_existing_path(self): 787 | """Test is_shared - path does not exist""" 788 | with self.assertRaises(owncloud.ResponseError) as e: 789 | self.client.is_shared(self.test_root + 'does_not_exist') 790 | self.assertEqual(e.exception.status_code, 404) 791 | 792 | def test_is_shared_not_shared_path(self): 793 | """Test is_shared - path does exist, but it's not shared yet""" 794 | path = self.test_root + 'not_shared_path.txt' 795 | self.assertTrue(self.client.put_file_contents(path, 'hello world!')) 796 | self.assertFalse(self.client.is_shared(path)) 797 | 798 | @data_provider(files) 799 | def test_is_shared(self, file_name): 800 | """Test is_shared""" 801 | path = self.test_root + file_name 802 | self.assertTrue(self.client.put_file_contents(path, 'hello world!')) 803 | 804 | self.client.share_file_with_link(path) 805 | self.assertTrue(self.client.is_shared(path)) 806 | self.assertTrue(self.client.delete(path)) 807 | 808 | @data_provider(files) 809 | def test_get_share_user(self, file_name): 810 | """Test get_share() for user share""" 811 | path = self.test_root + file_name 812 | self.assertTrue(self.client.put_file_contents(path, 'hello world!')) 813 | 814 | sinfo_run = self.client.share_file_with_user( 815 | path, 816 | self.share2user, 817 | perms=self.client.OCS_PERMISSION_READ | self.client.OCS_PERMISSION_SHARE 818 | ) 819 | sinfo = self.client.get_share(sinfo_run.get_id()) 820 | self.assertIsInstance(sinfo, owncloud.ShareInfo) 821 | share_id = sinfo.get_id() 822 | self.assertGreater(share_id, 0) 823 | self.assertEqual(sinfo_run.get_id(), share_id) 824 | self.assertIsInstance(sinfo.get_id(), int) 825 | self.assertEqual(sinfo.get_share_type(), self.client.OCS_SHARE_TYPE_USER) 826 | self.assertEqual(sinfo.get_share_with(), self.share2user) 827 | self.assertEqual(sinfo.get_path(), path) 828 | self.assertEqual( 829 | sinfo.get_permissions(), 830 | self.client.OCS_PERMISSION_READ | self.client.OCS_PERMISSION_SHARE 831 | ) 832 | self.assertIsInstance(sinfo.get_share_time(), datetime.datetime) 833 | self.assertIsNone(sinfo.get_expiration()) 834 | self.assertIsNone(sinfo.get_token()) 835 | self.assertEqual(sinfo.get_uid_owner(), Config['owncloud_login']) 836 | self.assertIsInstance(sinfo.get_displayname_owner(), six.string_types) 837 | 838 | @data_provider(files) 839 | def test_get_share_public_link(self, file_name): 840 | """Test get_share() for public link share""" 841 | path = self.test_root + file_name 842 | self.assertTrue(self.client.put_file_contents(path, 'hello world!')) 843 | 844 | sinfo_run = self.client.share_file_with_link(path) 845 | sinfo = self.client.get_share(sinfo_run.get_id()) 846 | self.assertIsInstance(sinfo, owncloud.ShareInfo) 847 | self.assertIsNotNone(sinfo) 848 | share_id = sinfo.get_id() 849 | self.assertGreater(share_id, 0) 850 | self.assertEqual(sinfo_run.get_id(), share_id) 851 | self.assertIsInstance(sinfo.get_id(), int) 852 | self.assertEqual(sinfo.get_share_type(), self.client.OCS_SHARE_TYPE_LINK) 853 | self.assertIsNone(sinfo.get_share_with()) 854 | self.assertEqual(sinfo.get_path(), path) 855 | self.assertEqual(sinfo.get_permissions(), self.client.OCS_PERMISSION_READ) 856 | self.assertIsInstance(sinfo.get_share_time(), datetime.datetime) 857 | self.assertIsNone(sinfo.get_expiration()) 858 | self.assertIsInstance(sinfo.get_token(), six.string_types) 859 | self.assertEqual(sinfo.get_uid_owner(), Config['owncloud_login']) 860 | self.assertIsInstance(sinfo.get_displayname_owner(), six.string_types) 861 | 862 | def test_get_share_non_existing(self): 863 | """Test get_share - share with specified id does not exist""" 864 | with self.assertRaises(owncloud.ResponseError) as e: 865 | self.client.get_share(-1) 866 | self.assertEqual(e.exception.status_code, 404) 867 | 868 | def test_get_shares_non_existing_path(self): 869 | """Test get_shares - path does not exist""" 870 | with self.assertRaises(owncloud.ResponseError) as e: 871 | self.client.get_shares(self.test_root + 'does_not_exist') 872 | self.assertEqual(e.exception.status_code, 404) 873 | 874 | @data_provider(files) 875 | def test_get_shares(self, file_name): 876 | """Test get_shares""" 877 | self.assertTrue(self.client.put_file_contents(self.test_root + file_name, 'hello world!')) 878 | 879 | self.client.share_file_with_link(self.test_root + file_name) 880 | shares = self.client.get_shares(self.test_root + file_name) 881 | self.assertIsNotNone(shares) 882 | self.assertIsInstance(shares, list) 883 | 884 | shares = None 885 | with self.assertRaises(owncloud.ResponseError) as e: 886 | shares = self.client.get_shares(self.test_root + file_name, subfiles=True) 887 | self.assertIsNone(shares) 888 | self.assertEqual(e.exception.status_code, 400) 889 | 890 | shares = self.client.get_shares(self.test_root, reshares=True, subfiles=True) 891 | self.assertIsNotNone(shares) 892 | self.assertIsInstance(shares, list) 893 | self.assertGreater(len(shares), 0) 894 | 895 | self.assertTrue(self.client.put_file_contents(self.test_root + file_name + '2.txt', 'hello world!')) 896 | self.client.share_file_with_link(self.test_root + file_name + '2.txt') 897 | shares = self.client.get_shares(self.test_root, reshares=True, subfiles=True) 898 | self.assertIsNotNone(shares) 899 | self.assertIsInstance(shares, list) 900 | self.assertGreater(len(shares), 1) 901 | 902 | def test_get_shares_empty(self): 903 | """Test get shares with empty result""" 904 | file_name = 'test.txt' 905 | self.assertTrue(self.client.put_file_contents(self.test_root + file_name, 'hello world!')) 906 | 907 | # Get all shares 908 | shares = self.client.get_shares() 909 | self.assertEqual(shares, []) 910 | 911 | def test_update_share_wo_params(self): 912 | self.assertFalse(self.client.update_share(0)) 913 | 914 | def test_update_share_user(self): 915 | """Test updating a share parameters - user share""" 916 | path = self.test_root + 'update_share_user.txt' 917 | self.assertTrue(self.client.put_file_contents(path, 'hello world!')) 918 | 919 | share_info = self.client.share_file_with_user(path, self.share2user) 920 | share_id = share_info.get_id() 921 | maxPerms = self.client.OCS_PERMISSION_READ + self.client.OCS_PERMISSION_UPDATE + self.client.OCS_PERMISSION_SHARE 922 | self.assertTrue(self.client.update_share(share_id, perms=maxPerms)) 923 | perms = self.client.get_shares(path)[0].get_permissions() 924 | # now the permissions should be OCS_PERMISSION_ALL, 925 | # because we've shared it with a user 926 | self.assertEqual(int(perms), maxPerms) 927 | self.assertTrue(self.client.delete_share(share_id)) 928 | 929 | def test_update_share_public(self): 930 | """Test updating a share parameters - public share""" 931 | path = self.test_root + 'update_share_public.txt' 932 | self.assertTrue(self.client.put_file_contents(path, 'hello world!')) 933 | 934 | share_info = self.client.share_file_with_link(path) 935 | share_id = share_info.get_id() 936 | self.assertTrue(self.client.update_share(share_id, perms=self.client.OCS_PERMISSION_ALL)) 937 | perms = self.client.get_shares(path)[0].get_permissions() 938 | # permissions should still be OCS_PERMISSION_READ not OCS_PERMISSION_ALL, 939 | # because it's a public share 940 | self.assertEqual(int(perms), self.client.OCS_PERMISSION_READ) 941 | self.assertTrue(self.client.delete_share(share_id)) 942 | 943 | def test_update_share_public_upload(self): 944 | """Test updating a share parameters - public upload""" 945 | path = self.test_root + 'update_share_public_upload' 946 | self.client.mkdir(path) 947 | 948 | share_info = self.client.share_file_with_link(path) 949 | share_id = share_info.get_id() 950 | self.assertTrue(self.client.update_share(share_id, public_upload=True)) 951 | perms = self.client.get_shares(path)[0].get_permissions() 952 | 953 | # make sure the server did receive the upload permission 954 | self.assertTrue(perms & self.client.OCS_PERMISSION_CREATE > 0) 955 | self.assertTrue(perms & self.client.OCS_PERMISSION_UPDATE > 0) 956 | 957 | # test reverting to read only 958 | self.assertTrue(self.client.update_share(share_id, public_upload=False)) 959 | perms = self.client.get_shares(path)[0].get_permissions() 960 | self.assertEqual(int(perms), self.client.OCS_PERMISSION_READ) 961 | self.assertTrue(self.client.delete_share(share_id)) 962 | 963 | def test_update_share_password(self): 964 | """Test updating a share parameters - password""" 965 | path = self.test_root + 'update_share_password' 966 | self.client.mkdir(path) 967 | 968 | share_info = self.client.share_file_with_link(path) 969 | share_id = share_info.get_id() 970 | self.assertTrue(self.client.update_share(share_id, password="2hard2guess")) 971 | share_info = self.client.get_shares(path)[0] 972 | self.assertTrue(type(share_info.get_share_with_displayname()) is str) 973 | self.assertTrue(self.client.delete_share(share_id)) 974 | 975 | @skipIfDavVersionLessThan(1.0) 976 | class TestFileAccessDav1(TestFileAccess): 977 | def get_dav_endpoint_version(self): 978 | return 1 979 | 980 | class TestPrivateDataAccess(unittest.TestCase): 981 | 982 | def attrs(): 983 | return ( 984 | ('attr1', 'value1'), 985 | ('attr+plus space', 'value+plus space and/slash'), 986 | (u'属性1', u'值对1') 987 | ) 988 | 989 | def setUp(self): 990 | self.client = owncloud.Client(Config['owncloud_url']) 991 | self.client.login(Config['owncloud_login'], Config['owncloud_password']) 992 | self.app_name = Config['app_name'] 993 | 994 | def tearDown(self): 995 | # TODO: delete all attributes ? 996 | self.client.logout() 997 | 998 | @data_provider(attrs) 999 | def test_set_attribute(self, attr1, value1): 1000 | """Test setting an attribute""" 1001 | self.assertTrue(self.client.set_attribute(self.app_name, attr1, value1)) 1002 | self.assertTrue(self.client.delete_attribute(self.app_name, attr1)) 1003 | 1004 | @data_provider(attrs) 1005 | def test_get_attribute(self, attr1, value1): 1006 | """Test getting an attribute""" 1007 | self.assertTrue(self.client.set_attribute(self.app_name, attr1, value1)) 1008 | 1009 | self.assertEqual(self.client.get_attribute(self.app_name, attr1), value1) 1010 | self.assertEqual(self.client.get_attribute(self.app_name), [(attr1, value1)]) 1011 | self.assertTrue(self.client.delete_attribute(self.app_name, attr1)) 1012 | 1013 | def test_get_non_existing_attribute(self): 1014 | """Test getting the value of a non existing attribute""" 1015 | self.assertIsNone(self.client.get_attribute(self.app_name, 'unexist')) 1016 | 1017 | @data_provider(attrs) 1018 | def test_set_attribute_empty(self, attr1, value1): 1019 | """Test setting an attribute to an empty value""" 1020 | self.assertTrue(self.client.set_attribute(self.app_name, attr1, '')) 1021 | self.assertEqual(self.client.get_attribute(self.app_name, attr1), '') 1022 | self.assertEqual(self.client.get_attribute(self.app_name), [(attr1, '')]) 1023 | self.assertTrue(self.client.delete_attribute(self.app_name, attr1)) 1024 | 1025 | @data_provider(attrs) 1026 | def test_delete_attribute(self, attr1, value1): 1027 | """Test deleting an attribute""" 1028 | self.assertTrue(self.client.set_attribute(self.app_name, attr1, value1)) 1029 | self.assertEqual(self.client.get_attribute(self.app_name, attr1), value1) 1030 | 1031 | self.assertTrue(self.client.delete_attribute(self.app_name, attr1)) 1032 | 1033 | self.assertIsNone(self.client.get_attribute(self.app_name, attr1)) 1034 | self.assertEqual(self.client.get_attribute(self.app_name), []) 1035 | 1036 | 1037 | class TestUserAndGroupActions(unittest.TestCase): 1038 | 1039 | def setUp(self): 1040 | self.client = owncloud.Client(Config['owncloud_url']) 1041 | self.client.login(Config['owncloud_login'], Config['owncloud_password']) 1042 | self.groups_to_create = Config['groups_to_create'] 1043 | self.not_existing_group = Config['not_existing_group'] 1044 | self.test_group = Config['test_group'] 1045 | self.share2user = Config['owncloud_share2user'] 1046 | self.share2userPwd = 'Avcpwd4l!' 1047 | try: 1048 | self.apps = self.client.get_apps() 1049 | if not self.apps['provisioning_api']: 1050 | raise unittest.SkipTest("no API") 1051 | except owncloud.ResponseError: 1052 | raise unittest.SkipTest("no API") 1053 | 1054 | try: 1055 | self.client.create_user(self.share2user, self.share2userPwd) 1056 | except: 1057 | pass 1058 | try: 1059 | self.client.create_group(self.test_group) 1060 | except: 1061 | pass 1062 | 1063 | def tearDown(self): 1064 | for group in self.groups_to_create: 1065 | self.assertTrue(self.client.delete_group(group)) 1066 | 1067 | self.assertTrue(self.client.remove_user_from_group(self.share2user,self.test_group)) 1068 | try: 1069 | self.client.delete_user(self.share2user) 1070 | except: 1071 | pass 1072 | try: 1073 | self.client.delete_group(self.test_group) 1074 | except: 1075 | pass 1076 | 1077 | self.client.logout() 1078 | 1079 | def test_user_exists(self): 1080 | self.assertTrue(self.client.user_exists(Config['owncloud_login'])) 1081 | try: 1082 | self.client.create_user('ghost_user', 'ghost_pass') 1083 | self.client.delete_user('ghost_user') 1084 | self.assertFalse(self.client.user_exists('ghost_user')) 1085 | except: 1086 | pass 1087 | 1088 | def test_get_user(self): 1089 | output = self.client.get_user(Config['owncloud_login']) 1090 | expected_output = {'displayname': 'admin', 1091 | 'enabled': 'true', 1092 | 'email': None, 1093 | 'quota': {'total': '309355267452', 1094 | 'relative': '0', 1095 | 'used': '3261820', 1096 | 'free': '309352005632'} 1097 | } 1098 | self.assertEqual(output['displayname'], expected_output['displayname']) 1099 | self.assertEqual(output['enabled'], expected_output['enabled']) 1100 | self.assertEqual(output['email'], expected_output['email']) 1101 | self.assertTrue('total' in output['quota']) 1102 | self.assertTrue('relative' in output['quota']) 1103 | self.assertTrue('used' in output['quota']) 1104 | self.assertTrue('free' in output['quota']) 1105 | 1106 | def test_search_users(self): 1107 | user_name = Config['owncloud_login'] 1108 | users = self.client.search_users(user_name[:-1]) 1109 | self.assertIn(user_name, users) 1110 | 1111 | def test_get_users(self): 1112 | user_name = Config['owncloud_login'] 1113 | users = self.client.get_users() 1114 | self.assertIn(user_name, users) 1115 | 1116 | def test_set_user_attribute(self): 1117 | try: 1118 | self.client.create_user('ghost_user', 'ghost_pass') 1119 | except: 1120 | self.client.delete_user('ghost_user') 1121 | self.client.create_user('ghost_user', 'ghost_pass') 1122 | self.assertTrue(self.client.set_user_attribute('ghost_user','email','test@inf.org')) 1123 | self.assertTrue(self.client.set_user_attribute('ghost_user','password','secret7363*')) 1124 | self.assertEqual(self.client.get_user('ghost_user')['email'], 'test@inf.org') 1125 | self.client.delete_user('ghost_user') 1126 | 1127 | with self.assertRaises(owncloud.OCSResponseError) as e: 1128 | self.client.set_user_attribute(self.share2user,'email',"äöüää_sfsdf+$%/)%&=") 1129 | self.assertEqual(e.exception.status_code, 102) 1130 | #try to catch with general ResponseError 1131 | with self.assertRaises(owncloud.ResponseError) as e: 1132 | self.client.set_user_attribute(self.share2user,'email',"äöüää_sfsdf+$%/)%&=") 1133 | self.assertEqual(e.exception.status_code, 102) 1134 | 1135 | def test_create_existing_user(self): 1136 | with self.assertRaises(owncloud.OCSResponseError) as e: 1137 | self.client.create_user(self.share2user, self.share2userPwd) 1138 | self.assertEqual(e.exception.status_code, 102) 1139 | # try to catch with general ResponseError 1140 | with self.assertRaises(owncloud.ResponseError) as e: 1141 | self.client.create_user(self.share2user, self.share2userPwd) 1142 | self.assertEqual(e.exception.status_code, 102) 1143 | 1144 | def test_create_groups(self): 1145 | for group in self.groups_to_create: 1146 | self.assertTrue(self.client.create_group(group)) 1147 | self.assertTrue(self.client.group_exists(group)) 1148 | # try to create them again, that should raise and OCSResponseError with code 102 1149 | with self.assertRaises(owncloud.OCSResponseError) as e: 1150 | self.client.create_group(group) 1151 | self.assertEqual(e.exception.status_code, 102) 1152 | #try to catch with general ResponseError 1153 | with self.assertRaises(owncloud.ResponseError) as e: 1154 | self.client.create_group(group) 1155 | self.assertEqual(e.exception.status_code, 102) 1156 | 1157 | def test_get_groups(self): 1158 | test_group = Config['test_group'] 1159 | not_existing_group = Config['not_existing_group'] 1160 | groups = self.client.get_groups() 1161 | self.assertIn(test_group, groups) 1162 | self.assertNotIn(not_existing_group, groups) 1163 | 1164 | def test_get_group_members(self): 1165 | user_name = Config['owncloud_login'] 1166 | test_group = Config['test_group'] 1167 | admin_members = self.client.get_group_members('admin') 1168 | test_members = self.client.get_group_members(test_group) 1169 | self.assertIn(user_name, admin_members) 1170 | self.assertNotIn(user_name, test_members) 1171 | 1172 | def test_not_existing_group(self): 1173 | self.assertFalse(self.client.group_exists(self.not_existing_group)) 1174 | 1175 | def test_add_user_to_group_remove_user_from_group(self): 1176 | self.assertFalse(self.client.user_is_in_group(self.share2user,self.test_group)) 1177 | self.assertTrue(self.client.add_user_to_group(self.share2user,self.test_group)) 1178 | self.assertTrue(self.client.user_is_in_group(self.share2user,self.test_group)) 1179 | 1180 | # try to add the user to a not existing group, that should raise and OCSResponseError with code 102 1181 | with self.assertRaises(owncloud.OCSResponseError) as e: 1182 | self.client.add_user_to_group(self.share2user,self.not_existing_group) 1183 | self.assertEqual(e.exception.status_code, 102) 1184 | # try to catch with general ResponseError 1185 | with self.assertRaises(owncloud.ResponseError) as e: 1186 | self.client.add_user_to_group(self.share2user,self.not_existing_group) 1187 | self.assertEqual(e.exception.status_code, 102) 1188 | 1189 | self.assertTrue(self.client.remove_user_from_group(self.share2user,self.test_group)) 1190 | self.assertFalse(self.client.user_is_in_group(self.share2user,self.test_group)) 1191 | 1192 | # try to remove the user from a not existing group, that should raise and OCSResponseError with code 102 1193 | with self.assertRaises(owncloud.OCSResponseError) as e: 1194 | self.client.remove_user_from_group(self.share2user,self.not_existing_group) 1195 | self.assertEqual(e.exception.status_code, 102) 1196 | # try to catch with general ResponseError 1197 | with self.assertRaises(owncloud.ResponseError) as e: 1198 | self.client.remove_user_from_group(self.share2user,self.not_existing_group) 1199 | self.assertEqual(e.exception.status_code, 102) 1200 | 1201 | # try to remove user without giving group name 1202 | with self.assertRaises(owncloud.OCSResponseError) as e: 1203 | self.client.remove_user_from_group(self.share2user,'') 1204 | self.assertEqual(e.exception.status_code, 101) 1205 | 1206 | # try to remove not existing user from a group 1207 | with self.assertRaises(owncloud.OCSResponseError) as e: 1208 | self.client.remove_user_from_group("iGuessThisUserNameDoesNotExistInTheSystem",self.test_group) 1209 | self.assertEqual(e.exception.status_code, 103) 1210 | 1211 | 1212 | class TestApps(unittest.TestCase): 1213 | 1214 | def setUp(self): 1215 | self.client = owncloud.Client(Config['owncloud_url']) 1216 | self.client.login(Config['owncloud_login'], Config['owncloud_password']) 1217 | 1218 | def test_enable_app_disable_app(self): 1219 | self.assertTrue(self.client.enable_app('activity')) 1220 | self.assertTrue(self.client.disable_app('activity')) 1221 | 1222 | def tearDown(self): 1223 | self.client.logout() 1224 | 1225 | 1226 | class TestGetConfig(unittest.TestCase): 1227 | 1228 | def setUp(self): 1229 | self.client = owncloud.Client(Config['owncloud_url']) 1230 | self.client.login(Config['owncloud_login'], Config['owncloud_password']) 1231 | 1232 | def test_get_config(self): 1233 | """Test get_config() function""" 1234 | self.assertIsNotNone(self.client.get_config()) 1235 | 1236 | def test_get_version(self): 1237 | """Test get_version() function""" 1238 | version = self.client.get_version() 1239 | self.assertIsNotNone(version) 1240 | version_parts = version.split('.') 1241 | self.assertGreaterEqual(int(version_parts[0]), 5) 1242 | 1243 | def test_get_capabilities(self): 1244 | """Test get_capabilities() function""" 1245 | caps = self.client.get_capabilities() 1246 | # files app is always enabled 1247 | self.assertIsNotNone(caps['files']) 1248 | # and always has big file chunking enabled 1249 | self.assertEqual(caps['files']['bigfilechunking'], '1') 1250 | 1251 | def tearDown(self): 1252 | self.client.logout() 1253 | 1254 | 1255 | class TestLogin(unittest.TestCase): 1256 | 1257 | def setUp(self): 1258 | self.client = owncloud.Client(Config['owncloud_url']) 1259 | 1260 | def test_login(self): 1261 | with self.assertRaises(owncloud.HTTPResponseError) as e: 1262 | self.client.login("iGuessThisUserNameDoesNotExistInTheSystem","iGuessThisUserNameDoesNotExistInTheSystem") 1263 | self.assertEqual(e.exception.status_code, 401) 1264 | self.client.login(Config['owncloud_login'], Config['owncloud_password']) 1265 | 1266 | def tearDown(self): 1267 | self.client.logout() 1268 | 1269 | 1270 | class TestOCSRequest(unittest.TestCase): 1271 | 1272 | def setUp(self): 1273 | self.client = owncloud.Client(Config['owncloud_url']) 1274 | self.client.login(Config['owncloud_login'], Config['owncloud_password']) 1275 | 1276 | def test_make_request(self): 1277 | kwargs = { 1278 | 'accepted_codes': [100] 1279 | } 1280 | self.client.make_ocs_request( 1281 | 'GET', 1282 | '', 1283 | 'config', 1284 | **kwargs 1285 | ) 1286 | 1287 | def test_make_request_fail_unaccepted_code(self): 1288 | kwargs = { 1289 | 'accepted_codes': [102] 1290 | } 1291 | with self.assertRaises(owncloud.OCSResponseError) as e: 1292 | self.client.make_ocs_request( 1293 | 'GET', 1294 | '', 1295 | 'config', 1296 | **kwargs 1297 | ) 1298 | self.assertEqual(e.exception.status_code, 100) 1299 | 1300 | def tearDown(self): 1301 | self.client.logout() 1302 | 1303 | class TestPublicFolder(unittest.TestCase): 1304 | 1305 | def get_dav_endpoint_version(self): 1306 | return 0 1307 | 1308 | def files(): 1309 | return ( 1310 | ['test.txt'], 1311 | ['test space and + and #.txt'], 1312 | [u'文件.txt'] 1313 | ) 1314 | 1315 | def files_content(): 1316 | return ( 1317 | ['test.txt', b'Hello world!', 'subdir'], 1318 | ['test space and + and #.txt', b'Hello space with+plus#hash!', 'subdir with space + plus and #hash'], 1319 | [u'文件.txt', u'你好世界'.encode('utf-8'), u'文件夹'] 1320 | ) 1321 | 1322 | 1323 | def setUp(self): 1324 | self.temp_dir = tempfile.gettempdir() + '/pyocclient_test%s-%s/' % (int(time.time()), random.randint(1, 1000)) 1325 | os.mkdir(self.temp_dir) 1326 | 1327 | self.client = owncloud.Client(Config['owncloud_url'], dav_endpoint_version=self.get_dav_endpoint_version()) 1328 | self.client.login(Config['owncloud_login'], Config['owncloud_password']) 1329 | self.test_root = Config['test_root'] 1330 | if not self.test_root[-1] == '/': 1331 | self.test_root += '/' 1332 | if not self.test_root[0] == '/': 1333 | self.test_root = '/' + self.test_root 1334 | 1335 | # setting up the root dir 1336 | self.client.mkdir(self.test_root) 1337 | self.download_testfile = 'download_pyoctest.dat' 1338 | self.upload_testfile = 'upload_pyoctest.dat' 1339 | self.folder_password = 'secret' 1340 | temp_file = self.temp_dir + self.download_testfile 1341 | 1342 | # create pathing for different scenarios 1343 | self.public_folder_download = self.test_root + 'public_share_download/' # file download only 1344 | self.public_folder_upload = self.test_root + 'public_share_upload/' # file upload only 1345 | self.pw_public_folder_download = self.test_root + 'pw_public_share_download/' # password protected download 1346 | self.pw_public_folder_upload = self.test_root + 'pw_public_share_upload/' # password protected upload 1347 | 1348 | # make folder with public download only 1349 | self.client.mkdir(self.public_folder_download) 1350 | public_share_info = self.client.share_file_with_link(self.public_folder_download, perms=self.client.OCS_PERMISSION_READ) 1351 | self.public_download_link = public_share_info.get_link() 1352 | self.public_download_token = public_share_info.get_token() 1353 | 1354 | # populate folder with data 1355 | temp_file = self.temp_dir + self.download_testfile 1356 | self.__create_file(temp_file, 2 * 1024) 1357 | self.client.put_file(self.public_folder_download + self.download_testfile, temp_file, chunked=False) 1358 | 1359 | # make folder with public upload only 1360 | self.client.mkdir(self.public_folder_upload) 1361 | public_share_info = self.client.share_file_with_link(self.public_folder_upload, perms=self.client.OCS_PERMISSION_CREATE) 1362 | self.public_upload_link = public_share_info.get_link() 1363 | self.public_upload_token = public_share_info.get_token() 1364 | 1365 | # make password protected folder with public download only 1366 | self.client.mkdir(self.pw_public_folder_download) 1367 | public_share_info = self.client.share_file_with_link(self.pw_public_folder_download, perms=self.client.OCS_PERMISSION_READ, password=self.folder_password) 1368 | self.pw_public_download_link = public_share_info.get_link() 1369 | self.pw_public_download_token = public_share_info.get_token() 1370 | 1371 | # populate password protected folder with data 1372 | temp_file = self.temp_dir + self.download_testfile 1373 | self.__create_file(temp_file, 2 * 1024) 1374 | self.client.put_file(self.pw_public_folder_download + self.download_testfile, temp_file, chunked=False) 1375 | 1376 | # make password protected folder with public upload only 1377 | self.client.mkdir(self.pw_public_folder_upload) 1378 | public_share_info = self.client.share_file_with_link(self.pw_public_folder_upload, perms=self.client.OCS_PERMISSION_CREATE, password=self.folder_password) 1379 | self.pw_public_upload_link = public_share_info.get_link() 1380 | self.pw_public_upload_token = public_share_info.get_token() 1381 | 1382 | def tearDown(self): 1383 | self.client.delete(self.test_root) 1384 | self.client.logout() 1385 | self.anon_client.logout() 1386 | shutil.rmtree(self.temp_dir) 1387 | 1388 | @staticmethod 1389 | def __create_file(target_file, size): 1390 | file_handle = open(target_file, 'w') 1391 | dummy_data = '' 1392 | for i in range(0, 1024): 1393 | dummy_data += 'X' 1394 | 1395 | for i in range(0, int(size / 1024)): 1396 | # write in 1kb blocks 1397 | file_handle.write(dummy_data) 1398 | 1399 | dummy_data = '' 1400 | for i in range(0, size % 1024): 1401 | dummy_data += 'X' 1402 | 1403 | if (len(dummy_data) > 0): 1404 | file_handle.write(dummy_data) 1405 | 1406 | file_handle.close() 1407 | 1408 | def test_anon_login(self): 1409 | self.anon_client = owncloud.Client(Config['owncloud_url']) 1410 | self.anon_client.anon_login(self.public_download_token) 1411 | listing = self.anon_client.list('/') 1412 | self.assertEqual(len(listing), 1) 1413 | 1414 | # repeat with wrong token 1415 | self.anon_client = owncloud.Client(Config['owncloud_url']) 1416 | self.anon_client.anon_login('badtoken') 1417 | with self.assertRaises(owncloud.ResponseError) as e: 1418 | self.anon_client.list('/') 1419 | self.assertEqual(e.exception.status_code, 401) 1420 | 1421 | # repeat with upload only folder (listing not allowed) 1422 | self.anon_client = owncloud.Client(Config['owncloud_url']) 1423 | self.anon_client.anon_login(self.public_upload_token) 1424 | with self.assertRaises(owncloud.ResponseError) as e: 1425 | self.anon_client.list('/') 1426 | self.assertEqual(e.exception.status_code, 404) 1427 | 1428 | def test_anon_login_pw(self): 1429 | self.anon_client = owncloud.Client(Config['owncloud_url']) 1430 | self.anon_client.anon_login(self.pw_public_download_token, folder_password=self.folder_password) 1431 | listing = self.anon_client.list('/') 1432 | self.assertEqual(len(listing), 1) 1433 | 1434 | # repeat with wrong password 1435 | self.anon_client = owncloud.Client(Config['owncloud_url']) 1436 | self.anon_client.anon_login(self.pw_public_download_token, folder_password='wrongpassword') 1437 | with self.assertRaises(owncloud.ResponseError) as e: 1438 | self.anon_client.list('/') 1439 | self.assertEqual(e.exception.status_code, 401) 1440 | 1441 | # repeat with upload only folder (listing not allowed) 1442 | self.anon_client = owncloud.Client(Config['owncloud_url']) 1443 | self.anon_client.anon_login(self.pw_public_upload_token, folder_password=self.folder_password) 1444 | with self.assertRaises(owncloud.ResponseError) as e: 1445 | self.anon_client.list('/') 1446 | self.assertEqual(e.exception.status_code, 404) 1447 | 1448 | def test_from_link(self): 1449 | self.anon_client = owncloud.Client.from_public_link(self.public_download_link) 1450 | listing = self.anon_client.list('/') 1451 | self.assertEqual(len(listing), 1) 1452 | 1453 | # repeat with wrong link 1454 | self.anon_client = owncloud.Client.from_public_link(Config['owncloud_url'] + 'index.php/s/wronglink') 1455 | with self.assertRaises(owncloud.ResponseError) as e: 1456 | self.anon_client.list('/') 1457 | self.assertEqual(e.exception.status_code, 401) 1458 | 1459 | # repeat with upload only folder (listing not allowed) 1460 | self.anon_client = owncloud.Client.from_public_link(self.public_upload_link) 1461 | with self.assertRaises(owncloud.ResponseError) as e: 1462 | self.anon_client.list('/') 1463 | self.assertEqual(e.exception.status_code, 404) 1464 | 1465 | def test_from_link_pw(self): 1466 | self.anon_client = owncloud.Client.from_public_link(self.pw_public_download_link, folder_password=self.folder_password) 1467 | listing = self.anon_client.list('/') 1468 | self.assertEqual(len(listing), 1) 1469 | 1470 | # repeat with wrong password 1471 | self.anon_client = owncloud.Client.from_public_link(self.pw_public_download_link, folder_password='wrongpassword') 1472 | with self.assertRaises(owncloud.ResponseError) as e: 1473 | self.anon_client.list('/') 1474 | self.assertEqual(e.exception.status_code, 401) 1475 | 1476 | # repeat with upload only folder (listing not allowed) 1477 | self.anon_client = owncloud.Client.from_public_link(self.pw_public_upload_link, folder_password=self.folder_password) 1478 | with self.assertRaises(owncloud.ResponseError) as e: 1479 | self.anon_client.list('/') 1480 | self.assertEqual(e.exception.status_code, 404) 1481 | 1482 | 1483 | @data_provider(files_content) 1484 | def test_download_file(self, file_name, content, subdir): 1485 | """Test file download""" 1486 | # populate folder with data 1487 | temp_file = self.temp_dir + file_name 1488 | self.client.put_file_contents(self.public_folder_download + '/' + file_name, content) 1489 | 1490 | self.anon_client = owncloud.Client(Config['owncloud_url']) 1491 | self.anon_client.anon_login(self.public_download_token) 1492 | 1493 | self.assertTrue(self.anon_client.get_file('/' + file_name, temp_file)) 1494 | 1495 | f = open(temp_file, 'rb') 1496 | s = f.read() 1497 | f.close() 1498 | os.unlink(temp_file) 1499 | self.assertEqual(s, content) 1500 | 1501 | # ZIP Download requires new method for anon session. API URL differs from regular session. 1502 | # def test_download_dir(self, file_name, content, subdir): 1503 | 1504 | @data_provider(files_content) 1505 | def test_mkdir(self, file_name, content, subdir): 1506 | """Test subdirectory creation""" 1507 | self.anon_client = owncloud.Client(Config['owncloud_url']) 1508 | self.anon_client.anon_login(self.public_upload_token) 1509 | 1510 | self.assertTrue(self.anon_client.mkdir(subdir)) 1511 | # Only users with account can get file info in upload folders 1512 | self.assertIsNotNone(self.client.file_info(self.public_folder_upload + subdir)) 1513 | 1514 | @data_provider(files) 1515 | def test_upload_small_file(self, file_name): 1516 | """Test simple upload""" 1517 | temp_file = self.temp_dir + 'pyoctest.dat' 1518 | self.__create_file(temp_file, 2 * 1024) 1519 | self.anon_client = owncloud.Client(Config['owncloud_url']) 1520 | self.anon_client.anon_login(self.public_upload_token) 1521 | 1522 | self.assertTrue(self.anon_client.put_file('/' + file_name, temp_file, chunked=False)) 1523 | os.unlink(temp_file) 1524 | 1525 | file_info = self.client.file_info(self.public_folder_upload + file_name) 1526 | self.assertIsNotNone(file_info) 1527 | self.assertEqual(file_info.get_size(), 2 * 1024) 1528 | 1529 | @data_provider(files) 1530 | def test_upload_big_file(self, file_name): 1531 | """Test chunked upload""" 1532 | temp_file = self.temp_dir + 'pyoctest.dat' 1533 | self.__create_file(temp_file, 10 * 1024 * 1024) 1534 | 1535 | self.anon_client = owncloud.Client(Config['owncloud_url']) 1536 | self.anon_client.anon_login(self.public_upload_token) 1537 | 1538 | self.assertTrue(self.anon_client.put_file('/' + file_name, temp_file)) 1539 | os.unlink(temp_file) 1540 | 1541 | file_info = self.client.file_info(self.public_folder_upload + file_name) 1542 | self.assertIsNotNone(file_info) 1543 | self.assertEqual(file_info.get_size(), 10 * 1024 * 1024) 1544 | 1545 | @data_provider(files) 1546 | def test_drop_file(self, file_name): 1547 | """Test convenience wrapper""" 1548 | temp_file = self.temp_dir + file_name 1549 | self.__create_file(temp_file, 2 * 1024) 1550 | self.anon_client = owncloud.Client(Config['owncloud_url']) 1551 | self.anon_client.anon_login(self.public_upload_token) 1552 | 1553 | self.assertTrue(self.anon_client.drop_file(temp_file)) 1554 | os.unlink(temp_file) 1555 | 1556 | file_info = self.client.file_info(self.public_folder_upload + file_name) 1557 | self.assertIsNotNone(file_info) 1558 | self.assertEqual(file_info.get_size(), 2 * 1024) 1559 | 1560 | if __name__ == '__main__': 1561 | unittest.main() 1562 | -------------------------------------------------------------------------------- /runtests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | PYTHONPATH=. python owncloud/test/test.py 4 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [build_sphinx] 2 | source-dir = docs/source 3 | build-dir = docs/build 4 | all_files = 1 5 | 6 | [upload_sphinx] 7 | upload-dir = docs/build/html 8 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # vim: expandtab shiftwidth=4 softtabstop=4 4 | # 5 | from setuptools import setup 6 | import io 7 | 8 | version = '0.6' 9 | 10 | long_description = ( 11 | io.open('README.rst', encoding='utf-8').read() 12 | + '\n' + 13 | 'Contributors\n' 14 | '============\n' 15 | + '\n' + 16 | io.open('docs/source/CONTRIBUTORS.rst', encoding='utf-8').read() 17 | + '\n' + 18 | io.open('CHANGES.rst', encoding='utf-8').read() 19 | + '\n') 20 | 21 | setup( 22 | name='pyocclient', 23 | version=version, 24 | author='Vincent Petry', 25 | author_email='pvince81@owncloud.com', 26 | packages=['owncloud', 'owncloud.test'], 27 | url='https://github.com/owncloud/pyocclient/', 28 | license='LICENSE.txt', 29 | description='Python client library for ownCloud', 30 | long_description=long_description, 31 | install_requires=[ 32 | "requests >= 2.0.1", 33 | "six" 34 | ], 35 | classifiers=[ 36 | 'Programming Language :: Python', 37 | 'Programming Language :: Python :: 2', 38 | 'Programming Language :: Python :: 3', 39 | 'Development Status :: 4 - Beta', 40 | 'Environment :: Web Environment', 41 | 'Intended Audience :: Developers', 42 | 'Topic :: Internet :: WWW/HTTP', 43 | 'Topic :: Software Development :: Libraries :: Python Modules', 44 | 'License :: OSI Approved :: MIT License' 45 | ] 46 | ) 47 | -------------------------------------------------------------------------------- /travis-build/setup_oc_docker.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | OC_VERSION=$1 3 | 4 | docker pull owncloud:$OC_VERSION 5 | DOCKER_ID=$(docker run -d -p 80:80 owncloud:$OC_VERSION) 6 | 7 | # needed else occ isn't available directly... 8 | sleep 5 9 | 10 | docker exec -u www-data $DOCKER_ID ./occ maintenance:install --admin-user="admin" --admin-pass="admin" --database="sqlite" 11 | 12 | --------------------------------------------------------------------------------