From 44ffde7eda144f33c4315310193c59f8fddd5f72 Mon Sep 17 00:00:00 2001 From: Holger Weiss Date: Mon, 7 Mar 2016 00:17:49 +0100 Subject: [PATCH] Remove mod_http_upload The HTTP upload modules are included with ejabberd 15.10 and newer. --- mod_http_upload/COPYING | 342 ------ mod_http_upload/README.txt | 222 ---- mod_http_upload/conf/mod_http_upload.yml | 10 - mod_http_upload/examples/nginx-upload.conf | 25 - mod_http_upload/mod_http_upload.spec | 5 - mod_http_upload/src/mod_http_upload.erl | 994 ------------------ mod_http_upload/src/mod_http_upload_quota.erl | 353 ------- 7 files changed, 1951 deletions(-) delete mode 100644 mod_http_upload/COPYING delete mode 100644 mod_http_upload/README.txt delete mode 100644 mod_http_upload/conf/mod_http_upload.yml delete mode 100644 mod_http_upload/examples/nginx-upload.conf delete mode 100644 mod_http_upload/mod_http_upload.spec delete mode 100644 mod_http_upload/src/mod_http_upload.erl delete mode 100644 mod_http_upload/src/mod_http_upload_quota.erl diff --git a/mod_http_upload/COPYING b/mod_http_upload/COPYING deleted file mode 100644 index cc498bd..0000000 --- a/mod_http_upload/COPYING +++ /dev/null @@ -1,342 +0,0 @@ -As a special exception, the authors give permission to link this program -with the OpenSSL library and distribute the resulting binary. - - GNU GENERAL PUBLIC LICENSE - Version 2, June 1991 - - Copyright (C) 1989, 1991 Free Software Foundation, Inc., - 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The licenses for most software are designed to take away your -freedom to share and change it. By contrast, the GNU General Public -License is intended to guarantee your freedom to share and change free -software--to make sure the software is free for all its users. This -General Public License applies to most of the Free Software -Foundation's software and to any other program whose authors commit to -using it. (Some other Free Software Foundation software is covered by -the GNU Lesser General Public License instead.) You can apply it to -your programs, too. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -this service if you wish), that you receive source code or can get it -if you want it, that you can change the software or use pieces of it -in new free programs; and that you know you can do these things. - - To protect your rights, we need to make restrictions that forbid -anyone to deny you these rights or to ask you to surrender the rights. -These restrictions translate to certain responsibilities for you if you -distribute copies of the software, or if you modify it. - - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must give the recipients all the rights that -you have. You must make sure that they, too, receive or can get the -source code. And you must show them these terms so they know their -rights. - - We protect your rights with two steps: (1) copyright the software, and -(2) offer you this license which gives you legal permission to copy, -distribute and/or modify the software. - - Also, for each author's protection and ours, we want to make certain -that everyone understands that there is no warranty for this free -software. If the software is modified by someone else and passed on, we -want its recipients to know that what they have is not the original, so -that any problems introduced by others will not reflect on the original -authors' reputations. - - Finally, any free program is threatened constantly by software -patents. We wish to avoid the danger that redistributors of a free -program will individually obtain patent licenses, in effect making the -program proprietary. To prevent this, we have made it clear that any -patent must be licensed for everyone's free use or not licensed at all. - - The precise terms and conditions for copying, distribution and -modification follow. - - GNU GENERAL PUBLIC LICENSE - TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION - - 0. This License applies to any program or other work which contains -a notice placed by the copyright holder saying it may be distributed -under the terms of this General Public License. The "Program", below, -refers to any such program or work, and a "work based on the Program" -means either the Program or any derivative work under copyright law: -that is to say, a work containing the Program or a portion of it, -either verbatim or with modifications and/or translated into another -language. (Hereinafter, translation is included without limitation in -the term "modification".) Each licensee is addressed as "you". - -Activities other than copying, distribution and modification are not -covered by this License; they are outside its scope. The act of -running the Program is not restricted, and the output from the Program -is covered only if its contents constitute a work based on the -Program (independent of having been made by running the Program). -Whether that is true depends on what the Program does. - - 1. You may copy and distribute verbatim copies of the Program's -source code as you receive it, in any medium, provided that you -conspicuously and appropriately publish on each copy an appropriate -copyright notice and disclaimer of warranty; keep intact all the -notices that refer to this License and to the absence of any warranty; -and give any other recipients of the Program a copy of this License -along with the Program. - -You may charge a fee for the physical act of transferring a copy, and -you may at your option offer warranty protection in exchange for a fee. - - 2. You may modify your copy or copies of the Program or any portion -of it, thus forming a work based on the Program, and copy and -distribute such modifications or work under the terms of Section 1 -above, provided that you also meet all of these conditions: - - a) You must cause the modified files to carry prominent notices - stating that you changed the files and the date of any change. - - b) You must cause any work that you distribute or publish, that in - whole or in part contains or is derived from the Program or any - part thereof, to be licensed as a whole at no charge to all third - parties under the terms of this License. - - c) If the modified program normally reads commands interactively - when run, you must cause it, when started running for such - interactive use in the most ordinary way, to print or display an - announcement including an appropriate copyright notice and a - notice that there is no warranty (or else, saying that you provide - a warranty) and that users may redistribute the program under - these conditions, and telling the user how to view a copy of this - License. (Exception: if the Program itself is interactive but - does not normally print such an announcement, your work based on - the Program is not required to print an announcement.) - -These requirements apply to the modified work as a whole. If -identifiable sections of that work are not derived from the Program, -and can be reasonably considered independent and separate works in -themselves, then this License, and its terms, do not apply to those -sections when you distribute them as separate works. But when you -distribute the same sections as part of a whole which is a work based -on the Program, the distribution of the whole must be on the terms of -this License, whose permissions for other licensees extend to the -entire whole, and thus to each and every part regardless of who wrote it. - -Thus, it is not the intent of this section to claim rights or contest -your rights to work written entirely by you; rather, the intent is to -exercise the right to control the distribution of derivative or -collective works based on the Program. - -In addition, mere aggregation of another work not based on the Program -with the Program (or with a work based on the Program) on a volume of -a storage or distribution medium does not bring the other work under -the scope of this License. - - 3. You may copy and distribute the Program (or a work based on it, -under Section 2) in object code or executable form under the terms of -Sections 1 and 2 above provided that you also do one of the following: - - a) Accompany it with the complete corresponding machine-readable - source code, which must be distributed under the terms of Sections - 1 and 2 above on a medium customarily used for software interchange; or, - - b) Accompany it with a written offer, valid for at least three - years, to give any third party, for a charge no more than your - cost of physically performing source distribution, a complete - machine-readable copy of the corresponding source code, to be - distributed under the terms of Sections 1 and 2 above on a medium - customarily used for software interchange; or, - - c) Accompany it with the information you received as to the offer - to distribute corresponding source code. (This alternative is - allowed only for noncommercial distribution and only if you - received the program in object code or executable form with such - an offer, in accord with Subsection b above.) - -The source code for a work means the preferred form of the work for -making modifications to it. For an executable work, complete source -code means all the source code for all modules it contains, plus any -associated interface definition files, plus the scripts used to -control compilation and installation of the executable. However, as a -special exception, the source code distributed need not include -anything that is normally distributed (in either source or binary -form) with the major components (compiler, kernel, and so on) of the -operating system on which the executable runs, unless that component -itself accompanies the executable. - -If distribution of executable or object code is made by offering -access to copy from a designated place, then offering equivalent -access to copy the source code from the same place counts as -distribution of the source code, even though third parties are not -compelled to copy the source along with the object code. - - 4. You may not copy, modify, sublicense, or distribute the Program -except as expressly provided under this License. Any attempt -otherwise to copy, modify, sublicense or distribute the Program is -void, and will automatically terminate your rights under this License. -However, parties who have received copies, or rights, from you under -this License will not have their licenses terminated so long as such -parties remain in full compliance. - - 5. You are not required to accept this License, since you have not -signed it. However, nothing else grants you permission to modify or -distribute the Program or its derivative works. These actions are -prohibited by law if you do not accept this License. Therefore, by -modifying or distributing the Program (or any work based on the -Program), you indicate your acceptance of this License to do so, and -all its terms and conditions for copying, distributing or modifying -the Program or works based on it. - - 6. Each time you redistribute the Program (or any work based on the -Program), the recipient automatically receives a license from the -original licensor to copy, distribute or modify the Program subject to -these terms and conditions. You may not impose any further -restrictions on the recipients' exercise of the rights granted herein. -You are not responsible for enforcing compliance by third parties to -this License. - - 7. If, as a consequence of a court judgment or allegation of patent -infringement or for any other reason (not limited to patent issues), -conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot -distribute so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you -may not distribute the Program at all. For example, if a patent -license would not permit royalty-free redistribution of the Program by -all those who receive copies directly or indirectly through you, then -the only way you could satisfy both it and this License would be to -refrain entirely from distribution of the Program. - -If any portion of this section is held invalid or unenforceable under -any particular circumstance, the balance of the section is intended to -apply and the section as a whole is intended to apply in other -circumstances. - -It is not the purpose of this section to induce you to infringe any -patents or other property right claims or to contest validity of any -such claims; this section has the sole purpose of protecting the -integrity of the free software distribution system, which is -implemented by public license practices. Many people have made -generous contributions to the wide range of software distributed -through that system in reliance on consistent application of that -system; it is up to the author/donor to decide if he or she is willing -to distribute software through any other system and a licensee cannot -impose that choice. - -This section is intended to make thoroughly clear what is believed to -be a consequence of the rest of this License. - - 8. If the distribution and/or use of the Program is restricted in -certain countries either by patents or by copyrighted interfaces, the -original copyright holder who places the Program under this License -may add an explicit geographical distribution limitation excluding -those countries, so that distribution is permitted only in or among -countries not thus excluded. In such case, this License incorporates -the limitation as if written in the body of this License. - - 9. The Free Software Foundation may publish revised and/or new versions -of the General Public License from time to time. Such new versions will -be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - -Each version is given a distinguishing version number. If the Program -specifies a version number of this License which applies to it and "any -later version", you have the option of following the terms and conditions -either of that version or of any later version published by the Free -Software Foundation. If the Program does not specify a version number of -this License, you may choose any version ever published by the Free Software -Foundation. - - 10. If you wish to incorporate parts of the Program into other free -programs whose distribution conditions are different, write to the author -to ask for permission. For software which is copyrighted by the Free -Software Foundation, write to the Free Software Foundation; we sometimes -make exceptions for this. Our decision will be guided by the two goals -of preserving the free status of all derivatives of our free software and -of promoting the sharing and reuse of software generally. - - NO WARRANTY - - 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY -FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN -OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES -PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED -OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF -MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS -TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE -PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, -REPAIR OR CORRECTION. - - 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR -REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, -INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING -OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED -TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY -YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER -PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE -POSSIBILITY OF SUCH DAMAGES. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -convey the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - - Copyright (C) - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; either version 2 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along - with this program; if not, write to the Free Software Foundation, Inc., - 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - -Also add information on how to contact you by electronic and paper mail. - -If the program is interactive, make it output a short notice like this -when it starts in an interactive mode: - - Gnomovision version 69, Copyright (C) year name of author - Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, the commands you use may -be called something other than `show w' and `show c'; they could even be -mouse-clicks or menu items--whatever suits your program. - -You should also get your employer (if you work as a programmer) or your -school, if any, to sign a "copyright disclaimer" for the program, if -necessary. Here is a sample; alter the names: - - Yoyodyne, Inc., hereby disclaims all copyright interest in the program - `Gnomovision' (which makes passes at compilers) written by James Hacker. - - , 1 April 1989 - Ty Coon, President of Vice - -This General Public License does not permit incorporating your program into -proprietary programs. If your program is a subroutine library, you may -consider it more useful to permit linking proprietary applications with the -library. If this is what you want to do, use the GNU Lesser General -Public License instead of this License. diff --git a/mod_http_upload/README.txt b/mod_http_upload/README.txt deleted file mode 100644 index 23115c1..0000000 --- a/mod_http_upload/README.txt +++ /dev/null @@ -1,222 +0,0 @@ - - mod_http_upload - HTTP File Upload (XEP-0363) - - Author: Holger Weiss - Requirements: ejabberd 15.06, 15.07, or 15.09 - Note: This module comes bundled with ejabberd 15.10 and newer - - - DESCRIPTION - ----------- - -This module allows for requesting permissions to upload a file via HTTP. If -the request is accepted, the client receives a URL to use for uploading the -file and another URL from which that file can later be downloaded. - -Automatic quota management can be configured by also enabling -mod_http_upload_quota. - - - CONFIGURATION - ------------- - -In order to use this module, add configuration snippets such as the -following: - - listen: - # [...] - - - module: ejabberd_http - port: 5443 - tls: true - certfile: "/etc/ejabberd/example.com.pem" - request_handlers: - "": mod_http_upload - - access: - # [...] - soft_upload_quota: - all: 1000 # MiB - hard_upload_quota: - all: 1100 # MiB - - modules: - # [...] - mod_http_upload: - docroot: "/home/xmpp/upload" - put_url: "https://@HOST@:5443" - mod_http_upload_quota: - max_days: 100 - -The configurable mod_http_upload options are: - -- host (default: "upload.@HOST@") - - This option defines the JID for the HTTP upload service. The keyword - @HOST@ is replaced with the virtual host name. - -- name (default: "HTTP File Upload") - - This option defines the Service Discovery name for the HTTP upload - service. - -- access (default: 'local') - - This option defines the access rule to limit who is permitted to use the - HTTP upload service. The default value is 'local'. If no access rule of - that name exists, no user will be allowed to use the service. - -- max_size (default: 104857600) - - This option limits the acceptable file size. Either a number of bytes - (larger than zero) or 'infinity' must be specified. - -- secret_length (default: 40) - - This option defines the length of the random string included in the GET - and PUT URLs generated by mod_http_upload. The minimum length is 8 - characters, but it is recommended to choose a larger value. - -- jid_in_url (default: 'sha1') - - When this option is set to 'node', the node identifier of the user's JID - (i.e., the user name) is included in the GET and PUT URLs generated by - mod_http_upload. Otherwise, a SHA-1 hash of the user's bare JID is - included instead. - -- thumbnail: (default: 'true') - - This option specifies whether ejabberd should create thumbnails of - uploaded images. If a thumbnail is created, a element that - contains the download and some metadata is returned with the PUT - response. - -- file_mode (default: 'undefined') - - This option defines the permission bits of uploaded files. The bits are - specified as an octal number (see the chmod(1) manual page) within double - quotes. For example: "0644". - -- dir_mode (default: 'undefined') - - This option defines the permission bits of the 'docroot' directory and any - directories created during file uploads. The bits are specified as an - octal number (see the chmod(1) manual page) within double quotes. For - example: "0755". - -- docroot (default: "@HOME@/upload") - - Uploaded files are stored below the directory specified (as an absolute - path) with this option. The keyword @HOME@ is replaced with the home - directory of the user running ejabberd. - -- put_url (default: "http://@HOST@:5444") - - This option specifies the initial part of the PUT URLs used for file - uploads. The keyword @HOST@ is replaced with the virtual host name. - - NOTE: Different virtual hosts cannot use the same PUT URL. - -- get_url (default: $put_url) - - This option specifies the initial part of the GET URLs used for - downloading the files. By default, it is set to the same value as the - 'put_url', but you can set it to a different value in order to have the - files served by a proper HTTP server such as Nginx or Apache. The keyword - @HOST@ is replaced with the virtual host name. - - NOTE: If GET requests are handled by mod_http_upload, the 'get_url' must - match the 'put_url'. - -- service_url (default: 'undefined') - - If a 'service_url' is specified, HTTP upload slot requests are forwarded - to this external service instead of being handled by mod_http_upload - itself. Such a service is available as a Django app, for example: - - https://github.com/mathiasertl/django-xmpp-http-upload - - An HTTP GET query such as the following is issued whenever an - HTTP upload slot request is accepted as per the 'access' rule: - - http://localhost:5444/?jid=juliet%40example.com&size=10240&name=example.jpg - - In order to accept the request, the service must return an HTTP status - code of 200 or 201 and two lines of text/plain output. The first line is - forwarded to the XMPP client as the HTTP upload PUT URL, the second line - as the GET URL. - - In order to reject the request, the service should return one of the - following HTTP status codes: - - - 402 - In this case, a 'resource-constraint' error stanza is sent to the - client. Use this to indicate a temporary error after the client - exceeded a quota, for example. - - - 403 - In this case, a 'not-allowed' error stanza is sent to the client. Use - this to indicate a permanent error to a client that is not permitted to - upload files, for example. - - - 413 - In this case, a 'not-acceptable' error stanza is sent to the client. - Use this if the file size was too large, for example. - - In any other case, a 'service-unavailable' error stanza is sent to the - client. - -- custom_headers (default: []) - - This option specifies additional header fields to be included in all HTTP - responses. For example: - - custom_headers: - "Access-Control-Allow-Origin": "*" - "Access-Control-Allow-Methods": "OPTIONS, HEAD, GET, PUT" - -- rm_on_unregister (default: 'true') - - This option specifies whether files uploaded by a user should be removed - when that user is unregistered. It must be set to 'false' if this is not - desired. - -The configurable mod_http_upload_quota options are: - -- max_days (default: 'infinity') - - If a number larger than zero is specified, any files (and directories) - older than this number of days are removed from the subdirectories of the - 'docroot' directory, once per day. By default, this won't be done. - -- access_hard_quota (default: 'hard_upload_quota') - - This option defines which access rule is used to specify the "hard quota" - for the matching JIDs. That rule must yield a positive number for any JID - that is supposed to have a quota limit. This is the number of megabytes a - corresponding user may upload. When this threshold is exceeded, ejabberd - deletes the oldest files uploaded by that user until their disk usage - equals or falls below the specified soft quota (see below). - -- access_soft_quota (default: 'soft_upload_quota') - - This option defines which access rule is used to specify the "soft quota" - for the matching JIDs. That rule must yield a positive number of - megabytes for any JID that is supposed to have a quota limit. It is - recommended to make sure the soft quota will be smaller than the hard - quota. See the description of the 'access_hard_quota' option for details. - - NOTE: It's not necessary to specify the 'access_hard_quota' and - 'access_soft_quota' options in order to use the quota feature. You can - stick to the default names and just specify access rules such as the - following: - - access: - # [...] - soft_upload_quota: - all: 400 - hard_upload_quota: - all: 500 - - This sets a soft quota of 400 MiB and a hard quota of 500 MiB for all - users. diff --git a/mod_http_upload/conf/mod_http_upload.yml b/mod_http_upload/conf/mod_http_upload.yml deleted file mode 100644 index 96b7e1e..0000000 --- a/mod_http_upload/conf/mod_http_upload.yml +++ /dev/null @@ -1,10 +0,0 @@ -listen: - - - module: ejabberd_http - port: 5444 - request_handlers: - "": mod_http_upload - -modules: - mod_http_upload: {} - mod_http_upload_quota: {} diff --git a/mod_http_upload/examples/nginx-upload.conf b/mod_http_upload/examples/nginx-upload.conf deleted file mode 100644 index 4035447..0000000 --- a/mod_http_upload/examples/nginx-upload.conf +++ /dev/null @@ -1,25 +0,0 @@ -server { - listen *:443 ssl; - server_name upload.example.com; - ssl_certificate /path/to/upload.example.com.crt; - ssl_certificate_key /path/to/upload.example.com.key; - - # The `docroot' used by mod_http_upload. - root /home/xmpp/upload; - - # Shouldn't be smaller than the `max_size' accepted by mod_http_upload. - client_max_body_size 100m; - - location / { - # Pass all requests to ejabberd, except for GET and HEAD requests. - limit_except GET { - proxy_pass http://localhost:5444; - } - proxy_set_header Host $host; - - if ($request_method = GET) { - add_header X-Frame-Options DENY; - } - add_header Strict-Transport-Security "max-age=31536000"; - } -} diff --git a/mod_http_upload/mod_http_upload.spec b/mod_http_upload/mod_http_upload.spec deleted file mode 100644 index 7163084..0000000 --- a/mod_http_upload/mod_http_upload.spec +++ /dev/null @@ -1,5 +0,0 @@ -author: "Holger Weiss " -category: "http" -summary: "HTTP File Upload" -home: "https://github.com/processone/ejabberd-contrib/tree/master/" -url: "git@github.com:processone/ejabberd-contrib.git" diff --git a/mod_http_upload/src/mod_http_upload.erl b/mod_http_upload/src/mod_http_upload.erl deleted file mode 100644 index 4e451eb..0000000 --- a/mod_http_upload/src/mod_http_upload.erl +++ /dev/null @@ -1,994 +0,0 @@ -%%%------------------------------------------------------------------- -%%% File : mod_http_upload.erl -%%% Author : Holger Weiss -%%% Purpose : HTTP File Upload (XEP-0363) -%%% Created : 20 Aug 2015 by Holger Weiss -%%%------------------------------------------------------------------- - --module(mod_http_upload). --author('holger@zedat.fu-berlin.de'). - --define(GEN_SERVER, gen_server). --define(NS_HTTP_UPLOAD, <<"urn:xmpp:http:upload">>). --define(NS_HTTP_UPLOAD_OLD, <<"eu:siacs:conversations:http:upload">>). --define(NS_THUMBS_1, <<"urn:xmpp:thumbs:1">>). --define(SERVICE_REQUEST_TIMEOUT, 5000). % 5 seconds. --define(SLOT_TIMEOUT, 18000000). % 5 hours. --define(PROCNAME, ?MODULE). --define(FORMAT(Error), file:format_error(Error)). --define(URL_ENC(URL), binary_to_list(ejabberd_http:url_encode(URL))). --define(ADDR_TO_STR(IP), ejabberd_config:may_hide_data(jlib:ip_to_list(IP))). --define(STR_TO_INT(Str, B), jlib:binary_to_integer(iolist_to_binary(Str), B)). --define(DEFAULT_CONTENT_TYPE, <<"application/octet-stream">>). --define(CONTENT_TYPES, - [{<<".avi">>, <<"video/avi">>}, - {<<".bmp">>, <<"image/bmp">>}, - {<<".bz2">>, <<"application/x-bzip2">>}, - {<<".gif">>, <<"image/gif">>}, - {<<".gz">>, <<"application/x-gzip">>}, - {<<".jpeg">>, <<"image/jpeg">>}, - {<<".jpg">>, <<"image/jpeg">>}, - {<<".mp3">>, <<"audio/mpeg">>}, - {<<".mp4">>, <<"video/mp4">>}, - {<<".mpeg">>, <<"video/mpeg">>}, - {<<".mpg">>, <<"video/mpeg">>}, - {<<".ogg">>, <<"application/ogg">>}, - {<<".pdf">>, <<"application/pdf">>}, - {<<".png">>, <<"image/png">>}, - {<<".rtf">>, <<"application/rtf">>}, - {<<".svg">>, <<"image/svg+xml">>}, - {<<".tiff">>, <<"image/tiff">>}, - {<<".txt">>, <<"text/plain">>}, - {<<".wav">>, <<"audio/wav">>}, - {<<".webp">>, <<"image/webp">>}, - {<<".xz">>, <<"application/x-xz">>}, - {<<".zip">>, <<"application/zip">>}]). - --behaviour(?GEN_SERVER). --behaviour(gen_mod). - -%% gen_mod/supervisor callbacks. --export([start_link/3, - start/2, - stop/1, - mod_opt_type/1]). - -%% gen_server callbacks. --export([init/1, - handle_call/3, - handle_cast/2, - handle_info/2, - terminate/2, - code_change/3]). - -%% ejabberd_http callback. --export([process/2]). - -%% ejabberd_hooks callback. --export([remove_user/2]). - -%% Utility functions. --export([get_proc_name/2, - expand_home/1]). - --include("ejabberd.hrl"). --include("ejabberd_http.hrl"). --include("jlib.hrl"). --include("logger.hrl"). - --record(state, - {server_host :: binary(), - host :: binary(), - name :: binary(), - access :: atom(), - max_size :: pos_integer() | infinity, - secret_length :: pos_integer(), - jid_in_url :: sha1 | node, - file_mode :: integer() | undefined, - dir_mode :: integer() | undefined, - docroot :: binary(), - put_url :: binary(), - get_url :: binary(), - service_url :: binary() | undefined, - thumbnail :: boolean(), - slots = dict:new() :: term()}). % dict:dict() requires Erlang 17. - --record(media_info, - {type :: binary(), - height :: integer(), - width :: integer()}). - --type state() :: #state{}. --type slot() :: [binary(), ...]. --type media_info() :: #media_info{}. - -%%-------------------------------------------------------------------- -%% gen_mod/supervisor callbacks. -%%-------------------------------------------------------------------- - --spec start_link(binary(), atom(), gen_mod:opts()) - -> {ok, pid()} | ignore | {error, _}. - -start_link(ServerHost, Proc, Opts) -> - ?GEN_SERVER:start_link({local, Proc}, ?MODULE, {ServerHost, Opts}, []). - --spec start(binary(), gen_mod:opts()) -> {ok, _} | {ok, _, _} | {error, _}. - -start(ServerHost, Opts) -> - case gen_mod:get_opt(rm_on_unregister, Opts, - fun(B) when is_boolean(B) -> B end, - true) of - true -> - ejabberd_hooks:add(remove_user, ServerHost, ?MODULE, - remove_user, 50), - ejabberd_hooks:add(anonymous_purge_hook, ServerHost, ?MODULE, - remove_user, 50); - false -> - ok - end, - Proc = get_proc_name(ServerHost, ?PROCNAME), - Spec = {Proc, - {?MODULE, start_link, [ServerHost, Proc, Opts]}, - permanent, - 3000, - worker, - [?MODULE]}, - supervisor:start_child(ejabberd_sup, Spec). - --spec stop(binary()) -> ok. - -stop(ServerHost) -> - case gen_mod:get_module_opt(ServerHost, ?MODULE, rm_on_unregister, - fun(B) when is_boolean(B) -> B end, - true) of - true -> - ejabberd_hooks:delete(remove_user, ServerHost, ?MODULE, - remove_user, 50), - ejabberd_hooks:delete(anonymous_purge_hook, ServerHost, ?MODULE, - remove_user, 50); - false -> - ok - end, - Proc = get_proc_name(ServerHost, ?PROCNAME), - supervisor:terminate_child(ejabberd_sup, Proc), - supervisor:delete_child(ejabberd_sup, Proc). - --spec mod_opt_type(atom()) -> fun((term()) -> term()) | [atom()]. - -mod_opt_type(host) -> - fun iolist_to_binary/1; -mod_opt_type(name) -> - fun iolist_to_binary/1; -mod_opt_type(access) -> - fun(A) when is_atom(A) -> A end; -mod_opt_type(max_size) -> - fun(I) when is_integer(I), I > 0 -> I; - (infinity) -> infinity - end; -mod_opt_type(secret_length) -> - fun(I) when is_integer(I), I >= 8 -> I end; -mod_opt_type(jid_in_url) -> - fun(sha1) -> sha1; - (node) -> node - end; -mod_opt_type(file_mode) -> - fun(Mode) -> ?STR_TO_INT(Mode, 8) end; -mod_opt_type(dir_mode) -> - fun(Mode) -> ?STR_TO_INT(Mode, 8) end; -mod_opt_type(docroot) -> - fun iolist_to_binary/1; -mod_opt_type(put_url) -> - fun(<<"http://", _/binary>> = URL) -> URL; - (<<"https://", _/binary>> = URL) -> URL - end; -mod_opt_type(get_url) -> - fun(<<"http://", _/binary>> = URL) -> URL; - (<<"https://", _/binary>> = URL) -> URL - end; -mod_opt_type(service_url) -> - fun(<<"http://", _/binary>> = URL) -> URL; - (<<"https://", _/binary>> = URL) -> URL - end; -mod_opt_type(custom_headers) -> - fun(Headers) -> - lists:map(fun({K, V}) -> - {iolist_to_binary(K), iolist_to_binary(V)} - end, Headers) - end; -mod_opt_type(rm_on_unregister) -> - fun(B) when is_boolean(B) -> B end; -mod_opt_type(thumbnail) -> - fun(B) when is_boolean(B) -> B end; -mod_opt_type(_) -> - [host, name, access, max_size, secret_length, jid_in_url, file_mode, - dir_mode, docroot, put_url, get_url, service_url, custom_headers, - rm_on_unregister, thumbnail]. - -%%-------------------------------------------------------------------- -%% gen_server callbacks. -%%-------------------------------------------------------------------- - --spec init({binary(), gen_mod:opts()}) -> {ok, state()}. - -init({ServerHost, Opts}) -> - process_flag(trap_exit, true), - Host = gen_mod:get_opt_host(ServerHost, Opts, <<"upload.@HOST@">>), - Name = gen_mod:get_opt(name, Opts, - fun iolist_to_binary/1, - <<"HTTP File Upload">>), - Access = gen_mod:get_opt(access, Opts, - fun(A) when is_atom(A) -> A end, - local), - MaxSize = gen_mod:get_opt(max_size, Opts, - fun(I) when is_integer(I), I > 0 -> I; - (infinity) -> infinity - end, - 104857600), - SecretLength = gen_mod:get_opt(secret_length, Opts, - fun(I) when is_integer(I), I >= 8 -> I end, - 40), - JIDinURL = gen_mod:get_opt(jid_in_url, Opts, - fun(sha1) -> sha1; - (node) -> node - end, - sha1), - DocRoot = gen_mod:get_opt(docroot, Opts, - fun iolist_to_binary/1, - <<"@HOME@/upload">>), - FileMode = gen_mod:get_opt(file_mode, Opts, - fun(Mode) -> ?STR_TO_INT(Mode, 8) end), - DirMode = gen_mod:get_opt(dir_mode, Opts, - fun(Mode) -> ?STR_TO_INT(Mode, 8) end), - PutURL = gen_mod:get_opt(put_url, Opts, - fun(<<"http://", _/binary>> = URL) -> URL; - (<<"https://", _/binary>> = URL) -> URL - end, - <<"http://@HOST@:5444">>), - GetURL = gen_mod:get_opt(get_url, Opts, - fun(<<"http://", _/binary>> = URL) -> URL; - (<<"https://", _/binary>> = URL) -> URL - end, - PutURL), - ServiceURL = gen_mod:get_opt(service_url, Opts, - fun(<<"http://", _/binary>> = URL) -> URL; - (<<"https://", _/binary>> = URL) -> URL - end), - Thumbnail = gen_mod:get_opt(thumbnail, Opts, - fun(B) when is_boolean(B) -> B end, - true), - case ServiceURL of - undefined -> - ok; - <<"http://", _/binary>> -> - application:start(inets); - <<"https://", _/binary>> -> - application:start(inets), - application:start(crypto), - application:start(asn1), - application:start(public_key), - application:start(ssl) - end, - case DirMode of - undefined -> - ok; - Mode -> - file:change_mode(DocRoot, Mode) - end, - case Thumbnail of - true -> - case string:str(os:cmd("identify"), "Magick") of - 0 -> - ?ERROR_MSG("Cannot find 'identify' command, please install " - "ImageMagick or disable thumbnail creation", []); - _ -> - ok - end; - false -> - ok - end, - ejabberd_router:register_route(Host), - {ok, #state{server_host = ServerHost, host = Host, name = Name, - access = Access, max_size = MaxSize, - secret_length = SecretLength, jid_in_url = JIDinURL, - file_mode = FileMode, dir_mode = DirMode, - thumbnail = Thumbnail, - docroot = expand_home(str:strip(DocRoot, right, $/)), - put_url = expand_host(str:strip(PutURL, right, $/), ServerHost), - get_url = expand_host(str:strip(GetURL, right, $/), ServerHost), - service_url = ServiceURL}}. - --spec handle_call(_, {pid(), _}, state()) - -> {reply, {ok, pos_integer(), binary(), - pos_integer() | undefined, - pos_integer() | undefined}, state()} | - {reply, {error, binary()}, state()} | {noreply, state()}. - -handle_call({use_slot, Slot}, _From, #state{file_mode = FileMode, - dir_mode = DirMode, - get_url = GetPrefix, - thumbnail = Thumbnail, - docroot = DocRoot} = State) -> - case get_slot(Slot, State) of - {ok, {Size, Timer}} -> - timer:cancel(Timer), - NewState = del_slot(Slot, State), - Path = str:join([DocRoot | Slot], <<$/>>), - {reply, {ok, Size, Path, FileMode, DirMode, GetPrefix, Thumbnail}, - NewState}; - error -> - {reply, {error, <<"Invalid slot">>}, State} - end; -handle_call(get_docroot, _From, #state{docroot = DocRoot} = State) -> - {reply, {ok, DocRoot}, State}; -handle_call(Request, From, State) -> - ?ERROR_MSG("Got unexpected request from ~p: ~p", [From, Request]), - {noreply, State}. - --spec handle_cast(_, state()) -> {noreply, state()}. - -handle_cast(Request, State) -> - ?ERROR_MSG("Got unexpected request: ~p", [Request]), - {noreply, State}. - --spec handle_info(timeout | _, state()) -> {noreply, state()}. - -handle_info({route, From, To, #xmlel{name = <<"iq">>} = Stanza}, State) -> - Request = jlib:iq_query_info(Stanza), - {Reply, NewState} = case process_iq(From, Request, State) of - R when is_record(R, iq) -> - {R, State}; - {R, S} -> - {R, S}; - not_request -> - {none, State} - end, - if Reply /= none -> - ejabberd_router:route(To, From, jlib:iq_to_xml(Reply)); - true -> - ok - end, - {noreply, NewState}; -handle_info({slot_timed_out, Slot}, State) -> - NewState = del_slot(Slot, State), - {noreply, NewState}; -handle_info(Info, State) -> - ?ERROR_MSG("Got unexpected info: ~p", [Info]), - {noreply, State}. - --spec terminate(normal | shutdown | {shutdown, _} | _, _) -> ok. - -terminate(Reason, #state{server_host = ServerHost, host = Host}) -> - ?DEBUG("Stopping HTTP upload process for ~s: ~p", [ServerHost, Reason]), - ejabberd_router:unregister_route(Host), - ok. - --spec code_change({down, _} | _, state(), _) -> {ok, state()}. - -code_change(_OldVsn, #state{server_host = ServerHost} = State, _Extra) -> - ?DEBUG("Updating HTTP upload process for ~s", [ServerHost]), - {ok, State}. - -%%-------------------------------------------------------------------- -%% ejabberd_http callback. -%%-------------------------------------------------------------------- - --spec process([binary()], #request{}) - -> {pos_integer(), [{binary(), binary()}], binary()}. - -process(LocalPath, #request{method = Method, host = Host, ip = IP}) - when length(LocalPath) < 3, - Method == 'PUT' orelse - Method == 'GET' orelse - Method == 'HEAD' -> - ?DEBUG("Rejecting ~s request from ~s for ~s: Too few path components", - [Method, ?ADDR_TO_STR(IP), Host]), - http_response(Host, 404); -process(_LocalPath, #request{method = 'PUT', host = Host, ip = IP, - data = Data} = Request) -> - {Proc, Slot} = parse_http_request(Request), - case catch gen_server:call(Proc, {use_slot, Slot}) of - {ok, Size, Path, FileMode, DirMode, GetPrefix, Thumbnail} - when byte_size(Data) == Size -> - ?DEBUG("Storing file from ~s for ~s: ~s", - [?ADDR_TO_STR(IP), Host, Path]), - case store_file(Path, Data, FileMode, DirMode, - GetPrefix, Slot, Thumbnail) of - ok -> - http_response(Host, 201); - {ok, Headers, OutData} -> - http_response(Host, 201, Headers, OutData); - {error, Error} -> - ?ERROR_MSG("Cannot store file ~s from ~s for ~s: ~p", - [Path, ?ADDR_TO_STR(IP), Host, ?FORMAT(Error)]), - http_response(Host, 500) - end; - {ok, Size, Path, _FileMode, _DirMode, _GetPrefix, _Thumbnail} -> - ?INFO_MSG("Rejecting file ~s from ~s for ~s: Size is ~B, not ~B", - [Path, ?ADDR_TO_STR(IP), Host, byte_size(Data), Size]), - http_response(Host, 413); - {error, Error} -> - ?INFO_MSG("Rejecting file from ~s for ~s: ~p", - [?ADDR_TO_STR(IP), Host, Error]), - http_response(Host, 403); - Error -> - ?ERROR_MSG("Cannot handle PUT request from ~s for ~s: ~p", - [?ADDR_TO_STR(IP), Host, Error]), - http_response(Host, 500) - end; -process(_LocalPath, #request{method = Method, host = Host, ip = IP} = Request) - when Method == 'GET'; - Method == 'HEAD' -> - {Proc, [_UserDir, _RandDir, FileName] = Slot} = parse_http_request(Request), - case catch gen_server:call(Proc, get_docroot) of - {ok, DocRoot} -> - Path = str:join([DocRoot | Slot], <<$/>>), - case file:read_file(Path) of - {ok, Data} -> - ?INFO_MSG("Serving ~s to ~s", [Path, ?ADDR_TO_STR(IP)]), - ContentType = guess_content_type(FileName), - Headers1 = case ContentType of - <<"image/", _SubType/binary>> -> []; - <<"text/", _SubType/binary>> -> []; - _ -> - [{<<"Content-Disposition">>, - <<"attachment; filename=", - $", FileName/binary, $">>}] - end, - Headers2 = [{<<"Content-Type">>, ContentType} | Headers1], - http_response(Host, 200, Headers2, Data); - {error, eacces} -> - ?INFO_MSG("Cannot serve ~s to ~s: Permission denied", - [Path, ?ADDR_TO_STR(IP)]), - http_response(Host, 403); - {error, enoent} -> - ?INFO_MSG("Cannot serve ~s to ~s: No such file", - [Path, ?ADDR_TO_STR(IP)]), - http_response(Host, 404); - {error, eisdir} -> - ?INFO_MSG("Cannot serve ~s to ~s: Is a directory", - [Path, ?ADDR_TO_STR(IP)]), - http_response(Host, 404); - {error, Error} -> - ?INFO_MSG("Cannot serve ~s to ~s: ~s", - [Path, ?ADDR_TO_STR(IP), ?FORMAT(Error)]), - http_response(Host, 500) - end; - Error -> - ?ERROR_MSG("Cannot handle ~s request from ~s for ~s: ~p", - [Method, ?ADDR_TO_STR(IP), Host, Error]), - http_response(Host, 500) - end; -process(_LocalPath, #request{method = 'OPTIONS', host = Host, ip = IP}) -> - ?DEBUG("Responding to OPTIONS request from ~s for ~s", - [?ADDR_TO_STR(IP), Host]), - http_response(Host, 200); -process(_LocalPath, #request{method = Method, host = Host, ip = IP}) -> - ?DEBUG("Rejecting ~s request from ~s for ~s", - [Method, ?ADDR_TO_STR(IP), Host]), - http_response(Host, 405, [{<<"Allow">>, <<"OPTIONS, HEAD, GET, PUT">>}]). - -%%-------------------------------------------------------------------- -%% Exported utility functions. -%%-------------------------------------------------------------------- - --spec get_proc_name(binary(), atom()) -> atom(). - -get_proc_name(ServerHost, ModuleName) -> - PutURL = gen_mod:get_module_opt(ServerHost, ?MODULE, put_url, - fun(<<"http://", _/binary>> = URL) -> URL; - (<<"https://", _/binary>> = URL) -> URL; - (_) -> <<"http://@HOST@">> - end, - <<"http://@HOST@">>), - {ok, {_Scheme, _UserInfo, Host, _Port, Path, _Query}} = - http_uri:parse(binary_to_list(expand_host(PutURL, ServerHost))), - ProcPrefix = list_to_binary(string:strip(Host ++ Path, right, $/)), - gen_mod:get_module_proc(ProcPrefix, ModuleName). - --spec expand_home(binary()) -> binary(). - -expand_home(Subject) -> - {ok, [[Home]]} = init:get_argument(home), - Parts = binary:split(Subject, <<"@HOME@">>, [global]), - str:join(Parts, list_to_binary(Home)). - -%%-------------------------------------------------------------------- -%% Internal functions. -%%-------------------------------------------------------------------- - -%% XMPP request handling. - --spec process_iq(jid(), iq_request() | reply | invalid, state()) - -> {iq_reply(), state()} | iq_reply() | not_request. - -process_iq(_From, - #iq{type = get, xmlns = ?NS_DISCO_INFO, lang = Lang} = IQ, - #state{server_host = ServerHost, name = Name}) -> - AddInfo = ejabberd_hooks:run_fold(disco_info, ServerHost, [], - [ServerHost, ?MODULE, <<"">>, <<"">>]), - IQ#iq{type = result, - sub_el = [#xmlel{name = <<"query">>, - attrs = [{<<"xmlns">>, ?NS_DISCO_INFO}], - children = iq_disco_info(Lang, Name) ++ AddInfo}]}; -process_iq(From, - #iq{type = get, xmlns = XMLNS, lang = Lang, sub_el = SubEl} = IQ, - #state{server_host = ServerHost, access = Access} = State) - when XMLNS == ?NS_HTTP_UPLOAD; - XMLNS == ?NS_HTTP_UPLOAD_OLD -> - case acl:match_rule(ServerHost, Access, From) of - allow -> - case parse_request(SubEl, Lang) of - {ok, File, Size, ContentType} -> - case create_slot(State, From, File, Size, ContentType, - Lang) of - {ok, Slot} -> - {ok, Timer} = timer:send_after(?SLOT_TIMEOUT, - {slot_timed_out, - Slot}), - NewState = add_slot(Slot, Size, Timer, State), - SlotEl = slot_el(Slot, State, XMLNS), - {IQ#iq{type = result, sub_el = [SlotEl]}, NewState}; - {ok, PutURL, GetURL} -> - SlotEl = slot_el(PutURL, GetURL, XMLNS), - IQ#iq{type = result, sub_el = [SlotEl]}; - {error, Error} -> - IQ#iq{type = error, sub_el = [SubEl, Error]} - end; - {error, Error} -> - ?DEBUG("Cannot parse request from ~s", - [jlib:jid_to_string(From)]), - IQ#iq{type = error, sub_el = [SubEl, Error]} - end; - deny -> - ?DEBUG("Denying HTTP upload slot request from ~s", - [jlib:jid_to_string(From)]), - IQ#iq{type = error, sub_el = [SubEl, ?ERR_FORBIDDEN]} - end; -process_iq(_From, #iq{sub_el = SubEl} = IQ, _State) -> - IQ#iq{type = error, sub_el = [SubEl, ?ERR_NOT_ALLOWED]}; -process_iq(_From, reply, _State) -> - not_request; -process_iq(_From, invalid, _State) -> - not_request. - --spec parse_request(xmlel(), binary()) - -> {ok, binary(), pos_integer(), binary()} | {error, xmlel()}. - -parse_request(#xmlel{name = <<"request">>, attrs = Attrs} = Request, Lang) -> - case xml:get_attr(<<"xmlns">>, Attrs) of - {value, XMLNS} when XMLNS == ?NS_HTTP_UPLOAD; - XMLNS == ?NS_HTTP_UPLOAD_OLD -> - case {xml:get_subtag_cdata(Request, <<"filename">>), - xml:get_subtag_cdata(Request, <<"size">>), - xml:get_subtag_cdata(Request, <<"content-type">>)} of - {File, SizeStr, ContentType} when byte_size(File) > 0 -> - case catch jlib:binary_to_integer(SizeStr) of - Size when is_integer(Size), Size > 0 -> - {ok, File, Size, yield_content_type(ContentType)}; - _ -> - Text = <<"Please specify file size.">>, - {error, ?ERRT_BAD_REQUEST(Lang, Text)} - end; - _ -> - Text = <<"Please specify file name.">>, - {error, ?ERRT_BAD_REQUEST(Lang, Text)} - end; - _ -> - {error, ?ERR_BAD_REQUEST} - end; -parse_request(_El, _Lang) -> {error, ?ERR_BAD_REQUEST}. - --spec create_slot(state(), jid(), binary(), pos_integer(), binary(), binary()) - -> {ok, slot()} | {ok, binary(), binary()} | {error, xmlel()}. - -create_slot(#state{service_url = undefined, max_size = MaxSize}, - JID, File, Size, _ContentType, Lang) when MaxSize /= infinity, - Size > MaxSize -> - Text = <<"File larger than ", (jlib:integer_to_binary(MaxSize))/binary, - " Bytes.">>, - ?INFO_MSG("Rejecting file ~s from ~s (too large: ~B bytes)", - [File, jlib:jid_to_string(JID), Size]), - {error, ?ERRT_NOT_ACCEPTABLE(Lang, Text)}; -create_slot(#state{service_url = undefined, - jid_in_url = JIDinURL, - secret_length = SecretLength, - server_host = ServerHost, - docroot = DocRoot}, - JID, File, Size, _ContentType, Lang) -> - UserStr = make_user_string(JID, JIDinURL), - UserDir = <>, - case ejabberd_hooks:run_fold(http_upload_slot_request, ServerHost, allow, - [JID, UserDir, Size, Lang]) of - allow -> - RandStr = make_rand_string(SecretLength), - FileStr = make_file_string(File), - ?INFO_MSG("Got HTTP upload slot for ~s (file: ~s)", - [jlib:jid_to_string(JID), File]), - {ok, [UserStr, RandStr, FileStr]}; - deny -> - {error, ?ERR_SERVICE_UNAVAILABLE}; - #xmlel{} = Error -> - {error, Error} - end; -create_slot(#state{service_url = ServiceURL}, - #jid{luser = U, lserver = S} = JID, File, Size, ContentType, - _Lang) -> - Options = [{body_format, binary}, {full_result, false}], - HttpOptions = [{timeout, ?SERVICE_REQUEST_TIMEOUT}], - SizeStr = jlib:integer_to_binary(Size), - GetRequest = binary_to_list(ServiceURL) ++ - "?jid=" ++ ?URL_ENC(jlib:jid_to_string({U, S, <<"">>})) ++ - "&name=" ++ ?URL_ENC(File) ++ - "&size=" ++ ?URL_ENC(SizeStr) ++ - "&content_type=" ++ ?URL_ENC(ContentType), - case httpc:request(get, {GetRequest, []}, HttpOptions, Options) of - {ok, {Code, Body}} when Code >= 200, Code =< 299 -> - case binary:split(Body, <<$\n>>, [global, trim]) of - [<<"http", _/binary>> = PutURL, - <<"http", _/binary>> = GetURL] -> - ?INFO_MSG("Got HTTP upload slot for ~s (file: ~s)", - [jlib:jid_to_string(JID), File]), - {ok, PutURL, GetURL}; - Lines -> - ?ERROR_MSG("Can't parse data received for ~s from <~s>: ~p", - [jlib:jid_to_string(JID), ServiceURL, Lines]), - {error, ?ERR_SERVICE_UNAVAILABLE} - end; - {ok, {402, _Body}} -> - ?INFO_MSG("Got status code 402 for ~s from <~s>", - [jlib:jid_to_string(JID), ServiceURL]), - {error, ?ERR_RESOURCE_CONSTRAINT}; - {ok, {403, _Body}} -> - ?INFO_MSG("Got status code 403 for ~s from <~s>", - [jlib:jid_to_string(JID), ServiceURL]), - {error, ?ERR_NOT_ALLOWED}; - {ok, {413, _Body}} -> - ?INFO_MSG("Got status code 413 for ~s from <~s>", - [jlib:jid_to_string(JID), ServiceURL]), - {error, ?ERR_NOT_ACCEPTABLE}; - {ok, {Code, _Body}} -> - ?ERROR_MSG("Got unexpected status code for ~s from <~s>: ~B", - [jlib:jid_to_string(JID), ServiceURL, Code]), - {error, ?ERR_SERVICE_UNAVAILABLE}; - {error, Reason} -> - ?ERROR_MSG("Error requesting upload slot for ~s from <~s>: ~p", - [jlib:jid_to_string(JID), ServiceURL, Reason]), - {error, ?ERR_SERVICE_UNAVAILABLE} - end. - --spec add_slot(slot(), pos_integer(), timer:tref(), state()) -> state(). - -add_slot(Slot, Size, Timer, #state{slots = Slots} = State) -> - NewSlots = dict:store(Slot, {Size, Timer}, Slots), - State#state{slots = NewSlots}. - --spec get_slot(slot(), state()) -> {ok, {pos_integer(), timer:tref()}} | error. - -get_slot(Slot, #state{slots = Slots}) -> - dict:find(Slot, Slots). - --spec del_slot(slot(), state()) -> state(). - -del_slot(Slot, #state{slots = Slots} = State) -> - NewSlots = dict:erase(Slot, Slots), - State#state{slots = NewSlots}. - --spec slot_el(slot() | binary(), state() | binary(), binary()) -> xmlel(). - -slot_el(Slot, #state{put_url = PutPrefix, get_url = GetPrefix}, XMLNS) -> - PutURL = str:join([PutPrefix | Slot], <<$/>>), - GetURL = str:join([GetPrefix | Slot], <<$/>>), - slot_el(PutURL, GetURL, XMLNS); -slot_el(PutURL, GetURL, XMLNS) -> - #xmlel{name = <<"slot">>, - attrs = [{<<"xmlns">>, XMLNS}], - children = [#xmlel{name = <<"put">>, - children = [{xmlcdata, PutURL}]}, - #xmlel{name = <<"get">>, - children = [{xmlcdata, GetURL}]}]}. - --spec make_user_string(jid(), sha1 | node) -> binary(). - -make_user_string(#jid{luser = U, lserver = S}, sha1) -> - p1_sha:sha(<>); -make_user_string(#jid{luser = U}, node) -> - re:replace(U, <<"[^a-zA-Z0-9_.-]">>, <<$_>>, [global, {return, binary}]). - --spec make_file_string(binary()) -> binary(). - -make_file_string(File) -> - re:replace(File, <<"[^a-zA-Z0-9_.-]">>, <<$_>>, [global, {return, binary}]). - --spec make_rand_string(non_neg_integer()) -> binary(). - -make_rand_string(Length) -> - list_to_binary(make_rand_string([], Length)). - --spec make_rand_string(string(), non_neg_integer()) -> string(). - -make_rand_string(S, 0) -> S; -make_rand_string(S, N) -> make_rand_string([make_rand_char() | S], N - 1). - --spec make_rand_char() -> char(). - -make_rand_char() -> - map_int_to_char(crypto:rand_uniform(0, 62)). - --spec map_int_to_char(0..61) -> char(). - -map_int_to_char(N) when N =< 9 -> N + 48; % Digit. -map_int_to_char(N) when N =< 35 -> N + 55; % Upper-case character. -map_int_to_char(N) when N =< 61 -> N + 61. % Lower-case character. - --spec expand_host(binary(), binary()) -> binary(). - -expand_host(Subject, Host) -> - Parts = binary:split(Subject, <<"@HOST@">>, [global]), - str:join(Parts, Host). - --spec yield_content_type(binary()) -> binary(). - -yield_content_type(<<"">>) -> ?DEFAULT_CONTENT_TYPE; -yield_content_type(Type) -> Type. - --spec iq_disco_info(binary(), binary()) -> [xmlel()]. - -iq_disco_info(Lang, Name) -> - [#xmlel{name = <<"identity">>, - attrs = [{<<"category">>, <<"store">>}, - {<<"type">>, <<"file">>}, - {<<"name">>, translate:translate(Lang, Name)}]}, - #xmlel{name = <<"feature">>, - attrs = [{<<"var">>, ?NS_HTTP_UPLOAD}]}, - #xmlel{name = <<"feature">>, - attrs = [{<<"var">>, ?NS_HTTP_UPLOAD_OLD}]}]. - -%% HTTP request handling. - --spec parse_http_request(#request{}) -> {atom(), slot()}. - -parse_http_request(#request{host = Host, path = Path}) -> - PrefixLength = length(Path) - 3, - {ProcURL, Slot} = if PrefixLength > 0 -> - Prefix = lists:sublist(Path, PrefixLength), - {str:join([Host | Prefix], $/), - lists:nthtail(PrefixLength, Path)}; - true -> - {Host, Path} - end, - {gen_mod:get_module_proc(ProcURL, ?PROCNAME), Slot}. - --spec store_file(binary(), binary(), - integer() | undefined, - integer() | undefined, - binary(), slot(), boolean()) - -> ok | {ok, [{binary(), binary()}], binary()} | {error, term()}. - -store_file(Path, Data, FileMode, DirMode, GetPrefix, Slot, Thumbnail) -> - case do_store_file(Path, Data, FileMode, DirMode) of - ok when Thumbnail -> - case identify(Path) of - {ok, MediaInfo} -> - case convert(Path, MediaInfo) of - {ok, OutPath} -> - [UserDir, RandDir | _] = Slot, - FileName = filename:basename(OutPath), - URL = str:join([GetPrefix, UserDir, - RandDir, FileName], <<$/>>), - ThumbEl = thumb_el(OutPath, URL), - {ok, - [{<<"Content-Type">>, - <<"text/xml; charset=utf-8">>}], - xml:element_to_binary(ThumbEl)}; - pass -> - ok - end; - pass -> - ok - end; - ok -> - ok; - Err -> - Err - end. - --spec do_store_file(file:filename_all(), binary(), - integer() | undefined, - integer() | undefined) - -> ok | {error, term()}. - -do_store_file(Path, Data, FileMode, DirMode) -> - try - ok = filelib:ensure_dir(Path), - {ok, Io} = file:open(Path, [write, exclusive, raw]), - Ok = file:write(Io, Data), - ok = file:close(Io), - if is_integer(FileMode) -> - ok = file:change_mode(Path, FileMode); - FileMode == undefined -> - ok - end, - if is_integer(DirMode) -> - RandDir = filename:dirname(Path), - UserDir = filename:dirname(RandDir), - ok = file:change_mode(RandDir, DirMode), - ok = file:change_mode(UserDir, DirMode); - DirMode == undefined -> - ok - end, - ok = Ok % Raise an exception if file:write/2 failed. - catch - _:{badmatch, {error, Error}} -> - {error, Error}; - _:Error -> - {error, Error} - end. - --spec guess_content_type(binary()) -> binary(). - -guess_content_type(FileName) -> - mod_http_fileserver:content_type(FileName, - ?DEFAULT_CONTENT_TYPE, - ?CONTENT_TYPES). - --spec http_response(binary(), 100..599) - -> {pos_integer(), [{binary(), binary()}], binary()}. - -http_response(Host, Code) -> - http_response(Host, Code, []). - --spec http_response(binary(), 100..599, [{binary(), binary()}]) - -> {pos_integer(), [{binary(), binary()}], binary()}. - -http_response(Host, Code, ExtraHeaders) -> - Message = <<(code_to_message(Code))/binary, $\n>>, - http_response(Host, Code, ExtraHeaders, Message). - --spec http_response(binary(), 100..599, [{binary(), binary()}], binary()) - -> {pos_integer(), [{binary(), binary()}], binary()}. - -http_response(Host, Code, ExtraHeaders, Body) -> - ServerHeader = {<<"Server">>, <<"ejabberd ", (?VERSION)/binary>>}, - CustomHeaders = - gen_mod:get_module_opt(Host, ?MODULE, custom_headers, - fun(Headers) -> - lists:map(fun({K, V}) -> - {iolist_to_binary(K), - iolist_to_binary(V)} - end, Headers) - end, - []), - Headers = case proplists:is_defined(<<"Content-Type">>, ExtraHeaders) of - true -> - [ServerHeader | ExtraHeaders]; - false -> - [ServerHeader, {<<"Content-Type">>, <<"text/plain">>} | - ExtraHeaders] - end ++ CustomHeaders, - {Code, Headers, Body}. - --spec code_to_message(100..599) -> binary(). - -code_to_message(201) -> <<"Upload successful.">>; -code_to_message(403) -> <<"Forbidden.">>; -code_to_message(404) -> <<"Not found.">>; -code_to_message(405) -> <<"Method not allowed.">>; -code_to_message(413) -> <<"File size doesn't match requested size.">>; -code_to_message(500) -> <<"Internal server error.">>; -code_to_message(_Code) -> <<"">>. - -%%-------------------------------------------------------------------- -%% Image manipulation stuff. -%%-------------------------------------------------------------------- - --spec identify(binary()) -> {ok, media_info()} | pass. - -identify(Path) -> - Cmd = io_lib:format("identify -format 'ok %m %h %w' ~s", [Path]), - Res = string:strip(os:cmd(Cmd), right, $\n), - case string:tokens(Res, " ") of - ["ok", T, H, W] -> - {ok, #media_info{type = list_to_binary(string:to_lower(T)), - height = list_to_integer(H), - width = list_to_integer(W)}}; - _ -> - ?DEBUG("Cannot identify type of ~s: ~s", [Path, Res]), - pass - end. - --spec convert(binary(), media_info()) -> {ok, binary()} | pass. - -convert(Path, #media_info{type = T, width = W, height = H}) -> - if W * H >= 25000000 -> - ?DEBUG("The image ~s is more than 25 Mpix", [Path]), - pass; - W =< 300, H =< 300 -> - {ok, Path}; - T == <<"gif">>; T == <<"jpeg">>; T == <<"png">>; T == <<"webp">> -> - Dir = filename:dirname(Path), - FileName = <<(randoms:get_string())/binary, $., T/binary>>, - OutPath = filename:join(Dir, FileName), - Cmd = io_lib:format("convert -resize 300 ~s ~s", [Path, OutPath]), - case os:cmd(Cmd) of - "" -> - {ok, OutPath}; - Err -> - ?ERROR_MSG("Failed to convert ~s to ~s: ~s", - [Path, OutPath, string:strip(Err, right, $\n)]), - pass - end; - true -> - ?DEBUG("Won't call 'convert' for unknown type ~s", [T]), - pass - end. - --spec thumb_el(binary(), binary()) -> xmlel(). - -thumb_el(Path, URI) -> - ContentType = guess_content_type(Path), - case identify(Path) of - {ok, #media_info{height = H, width = W}} -> - #xmlel{name = <<"thumbnail">>, - attrs = [{<<"xmlns">>, ?NS_THUMBS_1}, - {<<"media-type">>, ContentType}, - {<<"uri">>, URI}, - {<<"height">>, jlib:integer_to_binary(H)}, - {<<"width">>, jlib:integer_to_binary(W)}]}; - pass -> - #xmlel{name = <<"thumbnail">>, - attrs = [{<<"xmlns">>, ?NS_THUMBS_1}, - {<<"uri">>, URI}, - {<<"media-type">>, ContentType}]} - end. - -%%-------------------------------------------------------------------- -%% Remove user. -%%-------------------------------------------------------------------- - --spec remove_user(binary(), binary()) -> ok. - -remove_user(User, Server) -> - ServerHost = jlib:nameprep(Server), - DocRoot = gen_mod:get_module_opt(ServerHost, ?MODULE, docroot, - fun iolist_to_binary/1, - <<"@HOME@/upload">>), - JIDinURL = gen_mod:get_module_opt(ServerHost, ?MODULE, jid_in_url, - fun(sha1) -> sha1; - (node) -> node - end, - sha1), - UserStr = make_user_string(jlib:make_jid(User, Server, <<"">>), JIDinURL), - UserDir = str:join([expand_home(DocRoot), UserStr], <<$/>>), - case del_tree(UserDir) of - ok -> - ?INFO_MSG("Removed HTTP upload directory of ~s@~s", [User, Server]); - {error, enoent} -> - ?DEBUG("Found no HTTP upload directory of ~s@~s", [User, Server]); - {error, Error} -> - ?ERROR_MSG("Cannot remove HTTP upload directory of ~s@~s: ~p", - [User, Server, ?FORMAT(Error)]) - end, - ok. - --spec del_tree(file:filename_all()) -> ok | {error, term()}. - -del_tree(Dir) when is_binary(Dir) -> - del_tree(binary_to_list(Dir)); -del_tree(Dir) -> - try - {ok, Entries} = file:list_dir(Dir), - lists:foreach(fun(Path) -> - case filelib:is_dir(Path) of - true -> - ok = del_tree(Path); - false -> - ok = file:delete(Path) - end - end, [Dir ++ "/" ++ Entry || Entry <- Entries]), - ok = file:del_dir(Dir) - catch - _:{badmatch, {error, Error}} -> - {error, Error}; - _:Error -> - {error, Error} - end. diff --git a/mod_http_upload/src/mod_http_upload_quota.erl b/mod_http_upload/src/mod_http_upload_quota.erl deleted file mode 100644 index a232e30..0000000 --- a/mod_http_upload/src/mod_http_upload_quota.erl +++ /dev/null @@ -1,353 +0,0 @@ -%%%------------------------------------------------------------------- -%%% File : mod_http_upload_quota.erl -%%% Author : Holger Weiss -%%% Purpose : Quota management for HTTP File Upload (XEP-0363) -%%% Created : 15 Oct 2015 by Holger Weiss -%%%------------------------------------------------------------------- - --module(mod_http_upload_quota). --author('holger@zedat.fu-berlin.de'). - --define(GEN_SERVER, gen_server). --define(PROCNAME, ?MODULE). --define(TIMEOUT, timer:hours(24)). --define(INITIAL_TIMEOUT, timer:minutes(10)). --define(FORMAT(Error), file:format_error(Error)). - --behaviour(?GEN_SERVER). --behaviour(gen_mod). - -%% gen_mod/supervisor callbacks. --export([start_link/3, - start/2, - stop/1, - mod_opt_type/1]). - -%% gen_server callbacks. --export([init/1, - handle_call/3, - handle_cast/2, - handle_info/2, - terminate/2, - code_change/3]). - -%% ejabberd_hooks callback. --export([handle_slot_request/5]). - --include("jlib.hrl"). --include("logger.hrl"). --include_lib("kernel/include/file.hrl"). - --record(state, - {server_host :: binary(), - access_soft_quota :: atom(), - access_hard_quota :: atom(), - max_days :: pos_integer() | infinity, - docroot :: binary(), - disk_usage = dict:new() :: term(), - timers :: [timer:tref()]}). - --type state() :: #state{}. - -%%-------------------------------------------------------------------- -%% gen_mod/supervisor callbacks. -%%-------------------------------------------------------------------- - --spec start_link(binary(), atom(), gen_mod:opts()) - -> {ok, pid()} | ignore | {error, _}. - -start_link(ServerHost, Proc, Opts) -> - ?GEN_SERVER:start_link({local, Proc}, ?MODULE, {ServerHost, Opts}, []). - --spec start(binary(), gen_mod:opts()) -> {ok, _} | {ok, _, _} | {error, _}. - -start(ServerHost, Opts) -> - Proc = mod_http_upload:get_proc_name(ServerHost, ?PROCNAME), - Spec = {Proc, - {?MODULE, start_link, [ServerHost, Proc, Opts]}, - permanent, - 3000, - worker, - [?MODULE]}, - supervisor:start_child(ejabberd_sup, Spec). - --spec stop(binary()) -> ok. - -stop(ServerHost) -> - Proc = mod_http_upload:get_proc_name(ServerHost, ?PROCNAME), - supervisor:terminate_child(ejabberd_sup, Proc), - supervisor:delete_child(ejabberd_sup, Proc). - --spec mod_opt_type(atom()) -> fun((term()) -> term()) | [atom()]. - -mod_opt_type(access_soft_quota) -> - fun(A) when is_atom(A) -> A end; -mod_opt_type(access_hard_quota) -> - fun(A) when is_atom(A) -> A end; -mod_opt_type(max_days) -> - fun(I) when is_integer(I), I > 0 -> I; - (infinity) -> infinity - end; -mod_opt_type(_) -> - [access_soft_quota, access_hard_quota, max_days]. - -%%-------------------------------------------------------------------- -%% gen_server callbacks. -%%-------------------------------------------------------------------- - --spec init({binary(), gen_mod:opts()}) -> {ok, state()}. - -init({ServerHost, Opts}) -> - process_flag(trap_exit, true), - AccessSoftQuota = gen_mod:get_opt(access_soft_quota, Opts, - fun(A) when is_atom(A) -> A end, - soft_upload_quota), - AccessHardQuota = gen_mod:get_opt(access_hard_quota, Opts, - fun(A) when is_atom(A) -> A end, - hard_upload_quota), - MaxDays = gen_mod:get_opt(max_days, Opts, - fun(I) when is_integer(I), I > 0 -> I; - (infinity) -> infinity - end, - infinity), - DocRoot1 = gen_mod:get_module_opt(ServerHost, mod_http_upload, docroot, - fun iolist_to_binary/1, - <<"@HOME@/upload">>), - DocRoot2 = mod_http_upload:expand_home(str:strip(DocRoot1, right, $/)), - Timers = if MaxDays == infinity -> []; - true -> - {ok, T1} = timer:send_after(?INITIAL_TIMEOUT, sweep), - {ok, T2} = timer:send_interval(?TIMEOUT, sweep), - [T1, T2] - end, - ejabberd_hooks:add(http_upload_slot_request, ServerHost, ?MODULE, - handle_slot_request, 50), - {ok, #state{server_host = ServerHost, - access_soft_quota = AccessSoftQuota, - access_hard_quota = AccessHardQuota, - max_days = MaxDays, - docroot = DocRoot2, - timers = Timers}}. - --spec handle_call(_, {pid(), _}, state()) -> {noreply, state()}. - -handle_call(Request, From, State) -> - ?ERROR_MSG("Got unexpected request from ~p: ~p", [From, Request]), - {noreply, State}. - --spec handle_cast(_, state()) -> {noreply, state()}. - -handle_cast({handle_slot_request, #jid{user = U, server = S} = JID, Path, Size}, - #state{server_host = ServerHost, - access_soft_quota = AccessSoftQuota, - access_hard_quota = AccessHardQuota, - disk_usage = DiskUsage} = State) -> - HardQuota = case acl:match_rule(ServerHost, AccessHardQuota, JID) of - Hard when is_integer(Hard), Hard > 0 -> - Hard * 1024 * 1024; - _ -> - 0 - end, - SoftQuota = case acl:match_rule(ServerHost, AccessSoftQuota, JID) of - Soft when is_integer(Soft), Soft > 0 -> - Soft * 1024 * 1024; - _ -> - 0 - end, - OldSize = case dict:find({U, S}, DiskUsage) of - {ok, Value} -> - Value; - error -> - undefined - end, - NewSize = case {HardQuota, SoftQuota} of - {0, 0} -> - ?DEBUG("No quota specified for ~s", - [jlib:jid_to_string(JID)]), - undefined; - {0, _} -> - ?WARNING_MSG("No hard quota specified for ~s", - [jlib:jid_to_string(JID)]), - enforce_quota(Path, Size, OldSize, SoftQuota, SoftQuota); - {_, 0} -> - ?WARNING_MSG("No soft quota specified for ~s", - [jlib:jid_to_string(JID)]), - enforce_quota(Path, Size, OldSize, HardQuota, HardQuota); - _ when SoftQuota > HardQuota -> - ?WARNING_MSG("Bad quota for ~s (soft: ~p, hard: ~p)", - [jlib:jid_to_string(JID), - SoftQuota, HardQuota]), - enforce_quota(Path, Size, OldSize, SoftQuota, SoftQuota); - _ -> - ?DEBUG("Enforcing quota for ~s", - [jlib:jid_to_string(JID)]), - enforce_quota(Path, Size, OldSize, SoftQuota, HardQuota) - end, - NewDiskUsage = if is_integer(NewSize) -> - dict:store({U, S}, NewSize, DiskUsage); - true -> - DiskUsage - end, - {noreply, State#state{disk_usage = NewDiskUsage}}; -handle_cast(Request, State) -> - ?ERROR_MSG("Got unexpected request: ~p", [Request]), - {noreply, State}. - --spec handle_info(_, state()) -> {noreply, state()}. - -handle_info(sweep, #state{server_host = ServerHost, - docroot = DocRoot, - max_days = MaxDays} = State) - when is_integer(MaxDays), MaxDays > 0 -> - ?DEBUG("Got 'sweep' message for ~s", [ServerHost]), - case file:list_dir(DocRoot) of - {ok, Entries} -> - BackThen = secs_since_epoch() - (MaxDays * 86400), - DocRootS = binary_to_list(DocRoot), - PathNames = lists:map(fun(Entry) -> - DocRootS ++ "/" ++ Entry - end, Entries), - UserDirs = lists:filter(fun filelib:is_dir/1, PathNames), - lists:foreach(fun(UserDir) -> - delete_old_files(UserDir, BackThen) - end, UserDirs); - {error, Error} -> - ?ERROR_MSG("Cannot open document root ~s: ~s", - [DocRoot, ?FORMAT(Error)]) - end, - {noreply, State}; -handle_info(Info, State) -> - ?ERROR_MSG("Got unexpected info: ~p", [Info]), - {noreply, State}. - --spec terminate(normal | shutdown | {shutdown, _} | _, _) -> ok. - -terminate(Reason, #state{server_host = ServerHost, timers = Timers}) -> - ?DEBUG("Stopping upload quota process for ~s: ~p", [ServerHost, Reason]), - ejabberd_hooks:delete(http_upload_slot_request, ServerHost, ?MODULE, - handle_slot_request, 50), - lists:foreach(fun(Timer) -> timer:cancel(Timer) end, Timers). - --spec code_change({down, _} | _, state(), _) -> {ok, state()}. - -code_change(_OldVsn, #state{server_host = ServerHost} = State, _Extra) -> - ?DEBUG("Updating upload quota process for ~s", [ServerHost]), - {ok, State}. - -%%-------------------------------------------------------------------- -%% ejabberd_hooks callback. -%%-------------------------------------------------------------------- - --spec handle_slot_request(term(), jid(), binary(), non_neg_integer(), binary()) - -> term(). - -handle_slot_request(allow, #jid{lserver = ServerHost} = JID, Path, Size, - _Lang) -> - Proc = mod_http_upload:get_proc_name(ServerHost, ?PROCNAME), - ?GEN_SERVER:cast(Proc, {handle_slot_request, JID, Path, Size}), - allow; -handle_slot_request(Acc, _JID, _Path, _Size, _Lang) -> Acc. - -%%-------------------------------------------------------------------- -%% Internal functions. -%%-------------------------------------------------------------------- - --spec enforce_quota(file:filename_all(), non_neg_integer(), - non_neg_integer() | undefined, non_neg_integer(), - non_neg_integer()) - -> non_neg_integer(). - -enforce_quota(_UserDir, SlotSize, OldSize, _MinSize, MaxSize) - when is_integer(OldSize), OldSize + SlotSize =< MaxSize -> - OldSize + SlotSize; -enforce_quota(UserDir, SlotSize, _OldSize, MinSize, MaxSize) -> - Files = lists:sort(fun({_PathA, _SizeA, TimeA}, {_PathB, _SizeB, TimeB}) -> - TimeA > TimeB - end, gather_file_info(UserDir)), - {DelFiles, OldSize, NewSize} = - lists:foldl(fun({_Path, Size, _Time}, {[], AccSize, AccSize}) - when AccSize + Size + SlotSize =< MinSize -> - {[], AccSize + Size, AccSize + Size}; - ({Path, Size, _Time}, {[], AccSize, AccSize}) -> - {[Path], AccSize + Size, AccSize}; - ({Path, Size, _Time}, {AccFiles, AccSize, NewSize}) -> - {[Path | AccFiles], AccSize + Size, NewSize} - end, {[], 0, 0}, Files), - if OldSize + SlotSize > MaxSize -> - lists:foreach(fun(File) -> del_file_and_dir(File) end, DelFiles), - file:del_dir(UserDir), % In case it's empty, now. - NewSize + SlotSize; - true -> - OldSize + SlotSize - end. - --spec delete_old_files(file:filename_all(), integer()) -> ok. - -delete_old_files(UserDir, CutOff) -> - FileInfo = gather_file_info(UserDir), - case [Path || {Path, _Size, Time} <- FileInfo, Time < CutOff] of - [] -> - ok; - OldFiles -> - lists:foreach(fun(File) -> del_file_and_dir(File) end, OldFiles), - file:del_dir(UserDir) % In case it's empty, now. - end. - --spec gather_file_info(file:filename_all()) - -> [{binary(), non_neg_integer(), non_neg_integer()}]. - -gather_file_info(Dir) when is_binary(Dir) -> - gather_file_info(binary_to_list(Dir)); -gather_file_info(Dir) -> - case file:list_dir(Dir) of - {ok, Entries} -> - lists:foldl(fun(Entry, Acc) -> - Path = Dir ++ "/" ++ Entry, - case file:read_file_info(Path, - [{time, posix}]) of - {ok, #file_info{type = directory}} -> - gather_file_info(Path) ++ Acc; - {ok, #file_info{type = regular, - mtime = Time, - size = Size}} -> - [{Path, Size, Time} | Acc]; - {ok, _Info} -> - ?DEBUG("Won't stat(2) non-regular file ~s", - [Path]), - Acc; - {error, Error} -> - ?ERROR_MSG("Cannot stat(2) ~s: ~s", - [Path, ?FORMAT(Error)]), - Acc - end - end, [], Entries); - {error, enoent} -> - ?DEBUG("Directory ~s doesn't exist", [Dir]), - []; - {error, Error} -> - ?ERROR_MSG("Cannot open directory ~s: ~s", [Dir, ?FORMAT(Error)]), - [] - end. - --spec del_file_and_dir(file:name_all()) -> ok. - -del_file_and_dir(File) -> - case file:delete(File) of - ok -> - ?INFO_MSG("Removed ~s", [File]), - Dir = filename:dirname(File), - case file:del_dir(Dir) of - ok -> - ?DEBUG("Removed ~s", [Dir]); - {error, Error} -> - ?DEBUG("Cannot remove ~s: ~s", [Dir, ?FORMAT(Error)]) - end; - {error, Error} -> - ?WARNING_MSG("Cannot remove ~s: ~s", [File, ?FORMAT(Error)]) - end. - --spec secs_since_epoch() -> non_neg_integer(). - -secs_since_epoch() -> - {MegaSecs, Secs, _MicroSecs} = os:timestamp(), - MegaSecs * 1000000 + Secs.