├── .github └── ISSUE_TEMPLATE │ └── feature_request.md ├── .gitignore ├── CNAME ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── Untitled Diagram.png ├── Untitled Diagram.xml ├── __init__.py ├── _config.yml ├── dedupfs ├── LICENSE ├── NOTES ├── README.md ├── TODO ├── __init__.py ├── dedupfs.py ├── fuse.py ├── get_memory_usage.py ├── lzo │ ├── README │ ├── lzomodule.c │ └── setup.py ├── my_formats.py └── tests.sh ├── doc ├── index.html └── static │ ├── css │ ├── bootstrap.min.css │ └── font-awesome.css │ ├── fonts │ ├── FontAwesome.otf │ ├── fontawesome-webfont.eot │ ├── fontawesome-webfont.svg │ ├── fontawesome-webfont.ttf │ ├── fontawesome-webfont.woff │ └── fontawesome-webfont.woff2 │ └── js │ ├── bootstrap.min.js │ ├── jquery-3.3.1.slim.min.js │ └── popper.min.js ├── download_service.py ├── img ├── ProjectDiagram.png └── demo.gif ├── requirements.txt ├── secret.py.template ├── telegram_client_x.py ├── telegram_create_session.py ├── tgcloud-schematics.xml └── tgcloud.service /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | 5 | --- 6 | 7 | **Is your feature request related to a problem? Please describe.** 8 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 9 | 10 | **Describe the solution you'd like** 11 | A clear and concise description of what you want to happen. 12 | 13 | **Describe alternatives you've considered** 14 | A clear and concise description of any alternative solutions or features you've considered. 15 | 16 | **Additional context** 17 | Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | /tg_access.py 3 | __pycache__ 4 | *.session 5 | /secret.py.bk.py 6 | /test.py 7 | /secret.py 8 | -------------------------------------------------------------------------------- /CNAME: -------------------------------------------------------------------------------- 1 | dev.tgcloud.xyz -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at slavikmipt@mediatube.xyz. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tgcloud 2 | ## UNDER DEVELOPMENT v1.1 3 | ![](img/demo.gif) 4 | - `secret.py` : rename `secret.py.template`, insert `api_hash` and `api_id` obtained with https://my.telegram.org 5 | 6 | - Install Python2.7 and Python3.6 7 | 8 | - Clone the repository 9 | ``` 10 | cd ~ 11 | git clone https://github.com/SlavikMIPT/tgcloud.git 12 | ``` 13 | - Install requirements for Python3.6 14 | 15 | `sudo pip3 install -r requirements.txt` 16 | - Create a session by running **from the project folder** 17 | 18 | `python3.6 telegram_create_session.py` 19 | 20 | - Install fuse bindings 21 | 22 | `sudo yum install python-fuse` 23 | 24 | - Create Mount Folder 25 | 26 | `mkdir storage` 27 | 28 | - Run VFS **from the project folder**: 29 | 30 | #### debug: 31 | 32 | `python2.7 dedupfs/dedupfs.py -df --block-size 20971520 -o auto_unmount -o hard_remove storage/` 33 | 34 | #### as a service: 35 | 36 | replace `` in `tgcloud.service` 37 | ``` 38 | sudo cp tgcloud.service /ect/systemd/system/ 39 | sudo systemctl enable tgcloud.service 40 | sudo systemctl daemon-reload 41 | sudo systemctl start tgcloud.service 42 | sudo systemctl status tgcloud.service -l 43 | ``` 44 | 45 | Version 1.1 46 | 47 | It works more vigorously, but still a raw prototype - just POC 48 | 49 | Can crash, it is unstable and slow in some cases . 50 | 51 | For tests, it is better to use a separate telegram profile. 52 | 53 | If banned - mail `recover@telegram.org` 54 | 55 | You can try to use web based [filebrowser](https://github.com/filebrowser/filebrowser) or SFTP 56 | 57 | You are welcome to collaborate - contact 58 | Telegram: [@SlavikMIPT](t.me/SlavikMIPT) 59 | Channel: [@MediaTube_stream](t.me/MediaTube_stream) 60 | -------------------------------------------------------------------------------- /Untitled Diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SlavikMIPT/tgcloud/15a44135e9d5510a80065573390e965198016eef/Untitled Diagram.png -------------------------------------------------------------------------------- /Untitled Diagram.xml: -------------------------------------------------------------------------------- 1 | 7Vxbd5s4EP41fnQOQlwfm4vbnu2edns5u33aI4Ow2WLkxXIS769fCRBGIAwYTNI0vARGQkLSNzPfjOTM4M3m8W2CtuvfiY+jma75jzN4O9N1HTga+8Mlh0wCNDeXrJLQz2VHwZfwPywq5tJ96OOdVJESEtFwKws9EsfYo5IMJQl5kKsFJJJ73aIVrgm+eCgS0ivzKP8z9Ok6lwPLPRa8w+FqnXfu6FZWsETej1VC9nHe40yHQXplxRsk2sqHulsjnzyURPBuBm8SQmh2t3m8wRGfXjFx4j16EF87g9druonYA2C3afGi4WXQ5WU2uATHtNxdU3sQATsAHjZNDeimj+Z5B/co2osOdCtibV0v2c2Kpn1kgoCwPsrfYv27J6JgvktB8YZVgNr28VgoWvmKI7xK0IZVuInI3hetsm/NGpY7Y+LSB1Tm4GEdUvxlizz+/MAwLU9KsUQg/7gcsGxu2HMYRTckIknaFFwC3w+4fEcT8gOXSoBmQxcXJQJVfAF8tFtjv9TBAm3CiCvPOxzdYxp6iBVEaImj6wJeoumYxLgoJYmPk0pJWrtofhWhHdcO/o2EDTmkvB9TUy6+WE2cUPxYEuVgeIvJBtPkwKrkpRDkrxyELcjh+nBUIujkOrQu6Q8QFVGuuqui7SP82E2OQDUaTbu2tNhnip0/koSuyYrEKLo7SivT45FN6OX3JQjgx5D+xWeNGYbs6bsoidlHlor44/d8fiXg7ChK6BtunJjA46vAO8rEizAqeor9eiUmLFX5B1N6yDGI9pTwlSyG9oGQbdFjBYJaeikh2Ii6Mt4FPFVNKrHEZ79iZ2vg2pF94uEGc2Lkxh8lKyxes9UITHCEaHgv9zcETXbdWI6OJr0MJ02GE4BuGVDgStMLwH3CScgGhJNfGmnGZZGmdm5TIE+f1o5VgKdLhqzmAV/h9ZPDq+njFKStK0fTdRVHE63stiiuypZlIsfWPJ7vDrFXlCanCVwhKze930YEMSK4YFiNs9um1oqmbj/e/Hb3uaUTFaEstzkOnQROnU5i4JvYVqmAa9kQWTW9H4XDuRKHM7Q6hyuaLXM4xxmOTWsCp/tK4S5J4RSMDU7lN80n9ZuO4VQI2ytfG82hPif/2Z7jENmJ+X7rI4rV/kWr+5ItiaIwXvHB4+Q+9HC5Sgc3DE654R5Ob1h3I6VXQN0fej5eOksVgqEFXeg3+MOa81PgrdEfmrbW7g9F+qLsD90RUhrDqRk8Sc2KlfeiEKcNshnHS55CxUkbQlqI0uVokY+wE3gqGFieg5fBJWAAgQwDoClwADUFL7LHSG25F3dtZcd2JEKZa7NfQ8KhxAh0cWHGRC4MdPBhcvTWwRuYKkPz+e72/RdW+se3u293JZ9Xid2azcqwELTm6i62OaC1m63FQsKoCmqyyiqBN8yZ6bIVM6HCmVkKZzaGETOmzqhWojuYbncd4ztG0AvBS2fo9qXtGzTr9m0qc2Y2WjM/vFcaD6ZBdI6icBVn1iNJkd5Ok3pbJVu5ebngJvEM21TI0nE1WKw+TIrvTDtOD0JdiebSbcdPZBfSkMQc9wwiXIeuuZViKIw+VCosCaVkwyrkk3+bzfzxhTe5nJLU0pIk/I8NARX2lvcfr76mmqLJVkBlqk/vmjZpxCh8Ud4KZdNZs7QGdOqWVnf1ERx880a8Mpt5PpbH0QhBE27b8q4DotUOnz/mkCqteAXMjhUZ4CzL8+rvbvkxkaaJ0FqmaNBQUhKXqYAWhTGeC2jyUuYeuK1VjBRtUmVLn9LmM0GXTPq5s6ye0SBIVbj2jTys/Ztph3l9dTUzb3uEsV1W0WWX9G3to3FVmGF6jnxEUYfNh6GwbNavHuJhHzJSnshWuDXHw54yQbB0TMOsM23Jz6RUPgq370qdKQlW1WcVzm0Mpu7K2zCWOI9Q8h/AUDB13RiBqgO7NfEkTu2JdQXNWOscQipjuOsDxe8/tgSOT7pNFyDfRmq4GZapGTW4pe3ln9K0QdeLbJi6DBbDqIHFNepYEaR9EPl2Lh/VVXddXlp0dvnsk1OPzibblxOdj37A4POnm2eh/723JcbWf8MyWvUfaNZlDAC8eFpH3Kty0zMpo2O+IOPQHJj21n5Nof2TbZfqCu2v4IUH59tGPciP+aOlqK719o+WTKZs3RFH/MsaYivoFDCN4XPgDjeAyux6LT/FQrb94/wHTmL+m4xneqppiTzHhypV0aFhmGfu4mY4q0MgX3K3nuhWrrc1wnKLpPplId804LYRjrEvDVQjHJorVfr4kCcUA+ThFuhOtYkTBIGuDi0bNnG6gVc7by0huAK6qR2vEdCrt9O1HrFft9QiVC19sN/h+fbAT4c+jS3rBod+CfRBcDjIjKsNHeLUwCA0wDPY3ZAzmHrzIUytBw3rTa06p5CKN1YJ8vmpmfKBXWQtLauBd35dh/F53FOGUJ1HnkkLbQUtbEDe6LRQ/GDyiU762i/2qO+IYYMqaTAVPmDz0bfCA9yTCIzq/y8R63cy+G3hiyWFL46jK8IXR8UPxojvm7fXyyuh/xIrUcnKT7sSxgSp1lMHaF6szZzulIxVN6lTnZIRfb/qMZtz/Un1+Gl/qG5pVlWRX8/AjaLdCkI9mXY379j+etptyadcJ9bu5mxvMeM49pLDlp5Ka5zIX/Q74ed56jj2xN6USkNaMDH+WWUI5U0taChSuLpqEUdIcNqtByAajgJpmupAVw0AGzbnJ5e/f1arDyoW7MrzFN1+QfN8UOHaEipcQ7GRczFUdFDt7Cd/uwlUG6fXz6jaFjAnU232ePx3WmlZ6d+Wwbv/AQ== -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SlavikMIPT/tgcloud/15a44135e9d5510a80065573390e965198016eef/__init__.py -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-slate -------------------------------------------------------------------------------- /dedupfs/LICENSE: -------------------------------------------------------------------------------- 1 | DedupFS is licensed under the MIT license. 2 | 3 | Copyright 2010 Peter Odding . 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 9 | of the Software, and to permit persons to whom the Software is furnished to do 10 | so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /dedupfs/NOTES: -------------------------------------------------------------------------------- 1 | The included fuse.py module includes a single-line bug fix to the fuse.py file 2 | included with Ubuntu's Python 2.6 package for the method Timespec.__init__(): 3 | 4 | 480c480 5 | < def __init__(self, name, **kw): 6 | --- 7 | > def __init__(self, **kw): 8 | 9 | During initial development I used the following resources: 10 | - http://sf.net/apps/mediawiki/fuse/index.php?title=FUSE_Python_Reference 11 | - http://linux.die.net/man/2/path_resolution 12 | - /usr/include/fuse/fuse.h :-( 13 | -------------------------------------------------------------------------------- /dedupfs/README.md: -------------------------------------------------------------------------------- 1 | # DedupFS: A deduplicating FUSE file system written in Python 2 | 3 | The Python program [dedupfs.py](http://github.com/xolox/dedupfs/blob/master/dedupfs.py) implements a file system in user space using [FUSE](http://en.wikipedia.org/wiki/Filesystem_in_Userspace). It's called DedupFS because the file system's primary feature is [data deduplication](http://en.wikipedia.org/wiki/Data_deduplication), which enables it to store virtually unlimited copies of files because unchanged data is only stored once. In addition to deduplication the file system also supports transparent compression using the compression methods [lzo](http://en.wikipedia.org/wiki/LZO), [zlib](http://en.wikipedia.org/wiki/zlib) and [bz2](http://en.wikipedia.org/wiki/bz2). These properties make the file system ideal for backups: I'm currently storing 250 GB worth of backups using only 8 GB of disk space. 4 | 5 | Several aspects of the design of DedupFS were inspired by [Venti](http://en.wikipedia.org/wiki/Venti) (ignoring the distributed aspect, for now…) and [ZFS](http://en.wikipedia.org/wiki/ZFS), though I've never personally used either. The [ArchiveFS](http://code.google.com/p/archivefs/) and [lessfs](http://www.lessfs.com/) projects share similar goals but have very different implementations. 6 | 7 | ## Usage 8 | 9 | The following shell commands show how to install and use the DedupFS file system on [Ubuntu](http://www.ubuntu.com/) (where it was developed): 10 | 11 | $ sudo apt-get install python-fuse 12 | $ git clone git://github.com/xolox/dedupfs.git 13 | $ mkdir mount_point 14 | $ python dedupfs/dedupfs.py mount_point 15 | # Now copy some files to mount_point/ and observe that the size of the two 16 | # databases doesn't grow much when you copy duplicate files again :-) 17 | # The two databases are by default stored in the following locations: 18 | # - ~/.dedupfs-metastore.sqlite3 contains the tree and meta data 19 | # - ~/.dedupfs-datastore.db contains the (compressed) data blocks 20 | 21 | ## Status 22 | 23 | Development on DedupFS began as a proof of concept to find out how much disk space the author could free by employing deduplication to store his daily backups. Since then it's become more or less usable as a way to archive old backups, i.e. for secondary storage deduplication. It's not recommended to use the file system for primary storage though, simply because the file system is too slow. I also wouldn't recommend depending on DedupFS just yet, at least until a proper set of automated tests has been written and successfully run to prove the correctness of the code (the tests are being worked on). 24 | 25 | The file system initially stored everything in a single [SQLite](http://www.sqlite.org/) database, but it turned out that after the database grew beyond 8 GB the write speed would drop from 8-12 MB/s to 2-3 MB/s. Therefor the file system now stores its data blocks in a separate database, which is a persistent key/value store managed by a [dbm](http://en.wikipedia.org/wiki/dbm) implementation like [gdbm](http://www.gnu.org/software/gdbm/gdbm.html) or [Berkeley DB](http://en.wikipedia.org/wiki/Berkeley_DB). 26 | 27 | ### Limitations 28 | 29 | In the current implementation a file's content needs to fit in a [cStringIO](http://docs.python.org/library/stringio.html#module-cStringIO) instance, which limits the maximum file size to your free RAM. Initially I implemented it this way because I was focusing on backups of web/mail servers, which don't contain files larger than 250 MB. Then I started copying virtual disk images and my file system blew up :-(. I know how to fix this but haven't implemented the change yet. 30 | 31 | ## Dependencies 32 | 33 | DedupFS was developed using Python 2.6, though it might also work on earlier versions. It definitely doesn't work with Python 3 yet though. It requires the [Python FUSE binding](http://sourceforge.net/apps/mediawiki/fuse/index.php?title=FUSE_Python_tutorial) in addition to several Python standard libraries like [anydbm](http://docs.python.org/library/anydbm.html), [sqlite3](http://docs.python.org/library/sqlite3.html), [hashlib](http://docs.python.org/library/hashlib.html) and [cStringIO](http://docs.python.org/library/stringio.html#module-cStringIO). 34 | 35 | ## Contact 36 | 37 | If you have questions, bug reports, suggestions, etc. the author can be contacted at . The latest version of DedupFS is available at and . 38 | 39 | ## License 40 | 41 | This software is licensed under the MIT license. 42 | © 2010 Peter Odding <>. 43 | -------------------------------------------------------------------------------- /dedupfs/TODO: -------------------------------------------------------------------------------- 1 | Here are some things on my to-do list, in no particular order: 2 | 3 | * Automatically switch to a larger block size to reduce the overhead for files 4 | that rarely change after being created (like >= 100MB video files :-) 5 | 6 | * Implement the fsync(datasync) API method? 7 | if datasync: 8 | only flush user data (file contents) 9 | else: 10 | flush user & meta data (file contents & attributes) 11 | 12 | * Implement rename() independently of link()/unlink() to improve performance? 13 | 14 | * Implement `--verify-reads` option that recalculates hashes when reading to 15 | check for data block corruption? 16 | 17 | * `report_disk_usage()` has become way too expensive for regular status 18 | reports because it takes more than a minute on a 7.0 GB database. The only 19 | way it might work was if the statistics are only retrieved from the database 20 | once and from then on kept up to date inside Python, but that seems like an 21 | awful lot of work. For now I've removed the call to `report_disk_usage()` 22 | from `print_stats()` and added a `--print-stats` command-line option that 23 | reports the disk usage and then exits. 24 | 25 | * Tag databases with a version number and implement automatic upgrades because 26 | I've grown tired of upgrading my database by hand :-) 27 | 28 | * Change the project name because `DedupFS` is already used by at least two 29 | other projects? One is a distributed file system which shouldn't cause too 30 | much confusion, but the other is a deduplicating file system as well :-\ 31 | 32 | * Support directory hard links without upsetting FUSE and add a command-line 33 | option that instructs `dedupfs.py` to search for identical subdirectories 34 | and replace them with directory hard links. 35 | 36 | * Support files that don't fit in RAM (virtual machine disk images…) 37 | -------------------------------------------------------------------------------- /dedupfs/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SlavikMIPT/tgcloud/15a44135e9d5510a80065573390e965198016eef/dedupfs/__init__.py -------------------------------------------------------------------------------- /dedupfs/fuse.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2001 Jeff Epler 3 | # Copyright (C) 2006 Csaba Henk 4 | # 5 | # This program can be distributed under the terms of the GNU LGPL. 6 | # See the file COPYING. 7 | # 8 | 9 | 10 | # suppress version mismatch warnings 11 | try: 12 | import warnings 13 | 14 | warnings.filterwarnings('ignore', 15 | 'Python C API version mismatch', 16 | RuntimeWarning, 17 | ) 18 | except: 19 | pass 20 | 21 | from string import join 22 | import sys 23 | from errno import * 24 | from os import environ 25 | import re 26 | from fuseparts import __version__ 27 | from fuseparts._fuse import main, FuseGetContext, FuseInvalidate 28 | from fuseparts._fuse import FuseError, FuseAPIVersion 29 | from fuseparts.subbedopts import SubOptsHive, SubbedOptFormatter 30 | from fuseparts.subbedopts import SubbedOptIndentedFormatter, SubbedOptParse 31 | from fuseparts.subbedopts import SUPPRESS_HELP, OptParseError 32 | from fuseparts.setcompatwrap import set 33 | 34 | ########## 35 | ### 36 | ### API specification API. 37 | ### 38 | ########## 39 | 40 | # The actual API version of this module 41 | FUSE_PYTHON_API_VERSION = (0, 2) 42 | 43 | 44 | def __getenv__(var, pattern='.', trans=lambda x: x): 45 | """ 46 | Fetch enviroment variable and optionally transform it. Return `None` if 47 | variable is unset. Bail out if value of variable doesn't match (optional) 48 | regex pattern. 49 | """ 50 | 51 | if var not in environ: 52 | return None 53 | val = environ[var] 54 | rpat = pattern 55 | if not isinstance(rpat, type(re.compile(''))): 56 | rpat = re.compile(rpat) 57 | if not rpat.search(val): 58 | raise RuntimeError("env var %s doesn't match required pattern %s" % \ 59 | (var, `pattern`)) 60 | return trans(val) 61 | 62 | 63 | def get_fuse_python_api(): 64 | if fuse_python_api: 65 | return fuse_python_api 66 | elif compat_0_1: 67 | # deprecated way of API specification 68 | return (0, 1) 69 | 70 | 71 | def get_compat_0_1(): 72 | return get_fuse_python_api() == (0, 1) 73 | 74 | 75 | # API version to be used 76 | fuse_python_api = __getenv__('FUSE_PYTHON_API', '^[\d.]+$', 77 | lambda x: tuple([int(i) for i in x.split('.')])) 78 | 79 | # deprecated way of API specification 80 | compat_0_1 = __getenv__('FUSE_PYTHON_COMPAT', '^(0.1|ALL)$', lambda x: True) 81 | 82 | fuse_python_api = get_fuse_python_api() 83 | 84 | 85 | ########## 86 | ### 87 | ### Parsing for FUSE. 88 | ### 89 | ########## 90 | 91 | 92 | class FuseArgs(SubOptsHive): 93 | """ 94 | Class representing a FUSE command line. 95 | """ 96 | 97 | fuse_modifiers = {'showhelp': '-ho', 98 | 'showversion': '-V', 99 | 'foreground': '-f'} 100 | 101 | def __init__(self): 102 | 103 | SubOptsHive.__init__(self) 104 | 105 | self.modifiers = {} 106 | self.mountpoint = None 107 | 108 | for m in self.fuse_modifiers: 109 | self.modifiers[m] = False 110 | 111 | def __str__(self): 112 | return '\n'.join(['< on ' + str(self.mountpoint) + ':', 113 | ' ' + str(self.modifiers), ' -o ']) + \ 114 | ',\n '.join(self._str_core()) + \ 115 | ' >' 116 | 117 | def getmod(self, mod): 118 | return self.modifiers[mod] 119 | 120 | def setmod(self, mod): 121 | self.modifiers[mod] = True 122 | 123 | def unsetmod(self, mod): 124 | self.modifiers[mod] = False 125 | 126 | def mount_expected(self): 127 | if self.getmod('showhelp'): 128 | return False 129 | if self.getmod('showversion'): 130 | return False 131 | return True 132 | 133 | def assemble(self): 134 | """Mangle self into an argument array""" 135 | 136 | self.canonify() 137 | args = [sys.argv and sys.argv[0] or "python"] 138 | if self.mountpoint: 139 | args.append(self.mountpoint) 140 | for m, v in self.modifiers.iteritems(): 141 | if v: 142 | args.append(self.fuse_modifiers[m]) 143 | 144 | opta = [] 145 | for o, v in self.optdict.iteritems(): 146 | opta.append(o + '=' + v) 147 | opta.extend(self.optlist) 148 | 149 | if opta: 150 | args.append("-o" + ",".join(opta)) 151 | 152 | return args 153 | 154 | def filter(self, other=None): 155 | """ 156 | Same as for SubOptsHive, with the following difference: 157 | if other is not specified, `Fuse.fuseoptref()` is run and its result 158 | will be used. 159 | """ 160 | 161 | if not other: 162 | other = Fuse.fuseoptref() 163 | 164 | return SubOptsHive.filter(self, other) 165 | 166 | 167 | class FuseFormatter(SubbedOptIndentedFormatter): 168 | 169 | def __init__(self, **kw): 170 | if not 'indent_increment' in kw: 171 | kw['indent_increment'] = 4 172 | SubbedOptIndentedFormatter.__init__(self, **kw) 173 | 174 | def store_option_strings(self, parser): 175 | SubbedOptIndentedFormatter.store_option_strings(self, parser) 176 | # 27 is how the lib stock help appears 177 | self.help_position = max(self.help_position, 27) 178 | self.help_width = self.width - self.help_position 179 | 180 | 181 | class FuseOptParse(SubbedOptParse): 182 | """ 183 | This class alters / enhances `SubbedOptParse` so that it's 184 | suitable for usage with FUSE. 185 | 186 | - When adding options, you can use the `mountopt` pseudo-attribute which 187 | is equivalent with adding a subopt for option ``-o`` 188 | (it doesn't require an option argument). 189 | 190 | - FUSE compatible help and version printing. 191 | 192 | - Error and exit callbacks are relaxed. In case of FUSE, the command 193 | line is to be treated as a DSL [#]_. You don't wanna this module to 194 | force an exit on you just because you hit a DSL syntax error. 195 | 196 | - Built-in support for conventional FUSE options (``-d``, ``-f`, ``-s``). 197 | The way of this can be tuned by keyword arguments, see below. 198 | 199 | .. [#] http://en.wikipedia.org/wiki/Domain-specific_programming_language 200 | 201 | Keyword arguments for initialization 202 | ------------------------------------ 203 | 204 | standard_mods 205 | Boolean [default is `True`]. 206 | Enables support for the usual interpretation of the ``-d``, ``-f`` 207 | options. 208 | 209 | fetch_mp 210 | Boolean [default is `True`]. 211 | If it's True, then the last (non-option) argument 212 | (if there is such a thing) will be used as the FUSE mountpoint. 213 | 214 | dash_s_do 215 | String: ``whine``, ``undef``, or ``setsingle`` [default is ``whine``]. 216 | The ``-s`` option -- traditionally for asking for single-threadedness -- 217 | is an oddball: single/multi threadedness of a fuse-py fs doesn't depend 218 | on the FUSE command line, we have direct control over it. 219 | 220 | Therefore we have two conflicting principles: 221 | 222 | - *Orthogonality*: option parsing shouldn't affect the backing `Fuse` 223 | instance directly, only via its `fuse_args` attribute. 224 | 225 | - *POLS*: behave like other FUSE based fs-es do. The stock FUSE help 226 | makes mention of ``-s`` as a single-threadedness setter. 227 | 228 | So, if we follow POLS and implement a conventional ``-s`` option, then 229 | we have to go beyond the `fuse_args` attribute and set the respective 230 | Fuse attribute directly, hence violating orthogonality. 231 | 232 | We let the fs authors make their choice: ``dash_s_do=undef`` leaves this 233 | option unhandled, and the fs author can add a handler as she desires. 234 | ``dash_s_do=setsingle`` enables the traditional behaviour. 235 | 236 | Using ``dash_s_do=setsingle`` is not problematic at all, but we want fs 237 | authors be aware of the particularity of ``-s``, therefore the default is 238 | the ``dash_s_do=whine`` setting which raises an exception for ``-s`` and 239 | suggests the user to read this documentation. 240 | 241 | dash_o_handler 242 | Argument should be a SubbedOpt instance (created with 243 | ``action="store_hive"`` if you want it to be useful). 244 | This lets you customize the handler of the ``-o`` option. For example, 245 | you can alter or suppress the generic ``-o`` entry in help output. 246 | """ 247 | 248 | def __init__(self, *args, **kw): 249 | 250 | self.mountopts = [] 251 | 252 | self.fuse_args = \ 253 | 'fuse_args' in kw and kw.pop('fuse_args') or FuseArgs() 254 | dsd = 'dash_s_do' in kw and kw.pop('dash_s_do') or 'whine' 255 | if 'fetch_mp' in kw: 256 | self.fetch_mp = bool(kw.pop('fetch_mp')) 257 | else: 258 | self.fetch_mp = True 259 | if 'standard_mods' in kw: 260 | smods = bool(kw.pop('standard_mods')) 261 | else: 262 | smods = True 263 | if 'fuse' in kw: 264 | self.fuse = kw.pop('fuse') 265 | if not 'formatter' in kw: 266 | kw['formatter'] = FuseFormatter() 267 | doh = 'dash_o_handler' in kw and kw.pop('dash_o_handler') 268 | 269 | SubbedOptParse.__init__(self, *args, **kw) 270 | 271 | if doh: 272 | self.add_option(doh) 273 | else: 274 | self.add_option('-o', action='store_hive', 275 | subopts_hive=self.fuse_args, help="mount options", 276 | metavar="opt,[opt...]") 277 | 278 | if smods: 279 | self.add_option('-f', action='callback', 280 | callback=lambda *a: self.fuse_args.setmod('foreground'), 281 | help=SUPPRESS_HELP) 282 | self.add_option('-d', action='callback', 283 | callback=lambda *a: self.fuse_args.add('debug'), 284 | help=SUPPRESS_HELP) 285 | 286 | if dsd == 'whine': 287 | def dsdcb(option, opt_str, value, parser): 288 | raise RuntimeError, """ 289 | 290 | ! If you want the "-s" option to work, pass 291 | ! 292 | ! dash_s_do='setsingle' 293 | ! 294 | ! to the Fuse constructor. See docstring of the FuseOptParse class for an 295 | ! explanation why is it not set by default. 296 | """ 297 | 298 | elif dsd == 'setsingle': 299 | def dsdcb(option, opt_str, value, parser): 300 | self.fuse.multithreaded = False 301 | 302 | elif dsd == 'undef': 303 | dsdcb = None 304 | else: 305 | raise ArgumentError, "key `dash_s_do': uninterpreted value " + str(dsd) 306 | 307 | if dsdcb: 308 | self.add_option('-s', action='callback', callback=dsdcb, 309 | help=SUPPRESS_HELP) 310 | 311 | def exit(self, status=0, msg=None): 312 | if msg: 313 | sys.stderr.write(msg) 314 | 315 | def error(self, msg): 316 | SubbedOptParse.error(self, msg) 317 | raise OptParseError, msg 318 | 319 | def print_help(self, file=sys.stderr): 320 | SubbedOptParse.print_help(self, file) 321 | print >> file 322 | self.fuse_args.setmod('showhelp') 323 | 324 | def print_version(self, file=sys.stderr): 325 | SubbedOptParse.print_version(self, file) 326 | self.fuse_args.setmod('showversion') 327 | 328 | def parse_args(self, args=None, values=None): 329 | o, a = SubbedOptParse.parse_args(self, args, values) 330 | if a and self.fetch_mp: 331 | self.fuse_args.mountpoint = a.pop() 332 | return o, a 333 | 334 | def add_option(self, *opts, **attrs): 335 | if 'mountopt' in attrs: 336 | if opts or 'subopt' in attrs: 337 | raise OptParseError( 338 | "having options or specifying the `subopt' attribute conflicts with `mountopt' attribute") 339 | opts = ('-o',) 340 | attrs['subopt'] = attrs.pop('mountopt') 341 | if not 'dest' in attrs: 342 | attrs['dest'] = attrs['subopt'] 343 | 344 | SubbedOptParse.add_option(self, *opts, **attrs) 345 | 346 | 347 | ########## 348 | ### 349 | ### The FUSE interface. 350 | ### 351 | ########## 352 | 353 | 354 | class ErrnoWrapper(object): 355 | 356 | def __init__(self, func): 357 | self.func = func 358 | 359 | def __call__(self, *args, **kw): 360 | try: 361 | return apply(self.func, args, kw) 362 | except (IOError, OSError), detail: 363 | # Sometimes this is an int, sometimes an instance... 364 | if hasattr(detail, "errno"): detail = detail.errno 365 | return -detail 366 | 367 | 368 | ########### Custom objects for transmitting system structures to FUSE 369 | 370 | class FuseStruct(object): 371 | 372 | def __init__(self, **kw): 373 | for k in kw: 374 | setattr(self, k, kw[k]) 375 | 376 | 377 | class Stat(FuseStruct): 378 | """ 379 | Auxiliary class which can be filled up stat attributes. 380 | The attributes are undefined by default. 381 | """ 382 | 383 | def __init__(self, **kw): 384 | self.st_mode = None 385 | self.st_ino = 0 386 | self.st_dev = 0 387 | self.st_nlink = None 388 | self.st_uid = 0 389 | self.st_gid = 0 390 | self.st_size = 0 391 | self.st_atime = 0 392 | self.st_mtime = 0 393 | self.st_ctime = 0 394 | 395 | FuseStruct.__init__(self, **kw) 396 | 397 | 398 | class StatVfs(FuseStruct): 399 | """ 400 | Auxiliary class which can be filled up statvfs attributes. 401 | The attributes are 0 by default. 402 | """ 403 | 404 | def __init__(self, **kw): 405 | self.f_bsize = 0 406 | self.f_frsize = 0 407 | self.f_blocks = 0 408 | self.f_bfree = 0 409 | self.f_bavail = 0 410 | self.f_files = 0 411 | self.f_ffree = 0 412 | self.f_favail = 0 413 | self.f_flag = 0 414 | self.f_namemax = 0 415 | 416 | FuseStruct.__init__(self, **kw) 417 | 418 | 419 | class Direntry(FuseStruct): 420 | """ 421 | Auxiliary class for carrying directory entry data. 422 | Initialized with `name`. Further attributes (each 423 | set to 0 as default): 424 | 425 | offset 426 | An integer (or long) parameter, used as a bookmark 427 | during directory traversal. 428 | This needs to be set it you want stateful directory 429 | reading. 430 | 431 | type 432 | Directory entry type, should be one of the stat type 433 | specifiers (stat.S_IFLNK, stat.S_IFBLK, stat.S_IFDIR, 434 | stat.S_IFCHR, stat.S_IFREG, stat.S_IFIFO, stat.S_IFSOCK). 435 | 436 | ino 437 | Directory entry inode number. 438 | 439 | Note that Python's standard directory reading interface is 440 | stateless and provides only names, so the above optional 441 | attributes doesn't make sense in that context. 442 | """ 443 | 444 | def __init__(self, name, **kw): 445 | self.name = name 446 | self.offset = 0 447 | self.type = 0 448 | self.ino = 0 449 | 450 | FuseStruct.__init__(self, **kw) 451 | 452 | 453 | class Flock(FuseStruct): 454 | """ 455 | Class for representing flock structures (cf. fcntl(3)). 456 | 457 | It makes sense to give values to the `l_type`, `l_start`, 458 | `l_len`, `l_pid` attributes (`l_whence` is not used by 459 | FUSE, see ``fuse.h``). 460 | """ 461 | 462 | def __init__(self, name, **kw): 463 | self.l_type = None 464 | self.l_start = None 465 | self.l_len = None 466 | self.l_pid = None 467 | 468 | FuseStruct.__init__(self, **kw) 469 | 470 | 471 | class Timespec(FuseStruct): 472 | """ 473 | Cf. struct timespec in time.h: 474 | http://www.opengroup.org/onlinepubs/009695399/basedefs/time.h.html 475 | """ 476 | 477 | def __init__(self, **kw): 478 | self.tv_sec = None 479 | self.tv_nsec = None 480 | 481 | FuseStruct.__init__(self, **kw) 482 | 483 | 484 | class FuseFileInfo(FuseStruct): 485 | 486 | def __init__(self, **kw): 487 | self.keep = False 488 | self.direct_io = False 489 | 490 | FuseStruct.__init__(self, **kw) 491 | 492 | 493 | ########## Interface for requiring certain features from your underlying FUSE library. 494 | 495 | def feature_needs(*feas): 496 | """ 497 | Get info about the FUSE API version needed for the support of some features. 498 | 499 | This function takes a variable number of feature patterns. 500 | 501 | A feature pattern is either: 502 | 503 | - an integer (directly referring to a FUSE API version number) 504 | - a built-in feature specifier string (meaning defined by dictionary) 505 | - a string of the form ``has_foo``, where ``foo`` is a filesystem method 506 | (refers to the API version where the method has been introduced) 507 | - a list/tuple of other feature patterns (matches each of its members) 508 | - a regexp (meant to be matched against the builtins plus ``has_foo`` 509 | patterns; can also be given by a string of the from "re:*") 510 | - a negated regexp (can be given by a string of the form "!re:*") 511 | 512 | If called with no arguments, then the list of builtins is returned, mapped 513 | to their meaning. 514 | 515 | Otherwise the function returns the smallest FUSE API version number which 516 | has all the matching features. 517 | 518 | Builtin specifiers worth to explicit mention: 519 | - ``stateful_files``: you want to use custom filehandles (eg. a file class). 520 | - ``*``: you want all features. 521 | - while ``has_foo`` makes sense for all filesystem method ``foo``, some 522 | of these can be found among the builtins, too (the ones which can be 523 | handled by the general rule). 524 | 525 | specifiers like ``has_foo`` refer to requirement that the library knows of 526 | the fs method ``foo``. 527 | """ 528 | 529 | fmap = {'stateful_files': 22, 530 | 'stateful_dirs': 23, 531 | 'stateful_io': ('stateful_files', 'stateful_dirs'), 532 | 'stateful_files_keep_cache': 23, 533 | 'stateful_files_direct_io': 23, 534 | 'keep_cache': ('stateful_files_keep_cache',), 535 | 'direct_io': ('stateful_files_direct_io',), 536 | 'has_opendir': ('stateful_dirs',), 537 | 'has_releasedir': ('stateful_dirs',), 538 | 'has_fsyncdir': ('stateful_dirs',), 539 | 'has_create': 25, 540 | 'has_access': 25, 541 | 'has_fgetattr': 25, 542 | 'has_ftruncate': 25, 543 | 'has_fsinit': ('has_init'), 544 | 'has_fsdestroy': ('has_destroy'), 545 | 'has_lock': 26, 546 | 'has_utimens': 26, 547 | 'has_bmap': 26, 548 | 'has_init': 23, 549 | 'has_destroy': 23, 550 | '*': '!re:^\*$'} 551 | 552 | if not feas: 553 | return fmap 554 | 555 | def resolve(args, maxva): 556 | 557 | for fp in args: 558 | if isinstance(fp, int): 559 | maxva[0] = max(maxva[0], fp) 560 | continue 561 | if isinstance(fp, list) or isinstance(fp, tuple): 562 | for f in fp: 563 | yield f 564 | continue 565 | ma = isinstance(fp, str) and re.compile("(!\s*|)re:(.*)").match(fp) 566 | if isinstance(fp, type(re.compile(''))) or ma: 567 | neg = False 568 | if ma: 569 | mag = ma.groups() 570 | fp = re.compile(mag[1]) 571 | neg = bool(mag[0]) 572 | for f in fmap.keys() + ['has_' + a for a in Fuse._attrs]: 573 | if neg != bool(re.search(fp, f)): 574 | yield f 575 | continue 576 | ma = re.compile("has_(.*)").match(fp) 577 | if ma and ma.groups()[0] in Fuse._attrs and not fp in fmap: 578 | yield 21 579 | continue 580 | yield fmap[fp] 581 | 582 | maxva = [0] 583 | while feas: 584 | feas = set(resolve(feas, maxva)) 585 | 586 | return maxva[0] 587 | 588 | 589 | def APIVersion(): 590 | """Get the API version of your underlying FUSE lib""" 591 | 592 | return FuseAPIVersion() 593 | 594 | 595 | def feature_assert(*feas): 596 | """ 597 | Takes some feature patterns (like in `feature_needs`). 598 | Raises a fuse.FuseError if your underlying FUSE lib fails 599 | to have some of the matching features. 600 | 601 | (Note: use a ``has_foo`` type feature assertion only if lib support 602 | for method ``foo`` is *necessary* for your fs. Don't use this assertion 603 | just because your fs implements ``foo``. The usefulness of ``has_foo`` 604 | is limited by the fact that we can't guarantee that your FUSE kernel 605 | module also supports ``foo``.) 606 | """ 607 | 608 | fav = APIVersion() 609 | 610 | for fea in feas: 611 | fn = feature_needs(fea) 612 | if fav < fn: 613 | raise FuseError( 614 | "FUSE API version %d is required for feature `%s' but only %d is available" % \ 615 | (fn, str(fea), fav)) 616 | 617 | 618 | ############# Subclass this. 619 | 620 | class Fuse(object): 621 | """ 622 | Python interface to FUSE. 623 | 624 | Basic usage: 625 | 626 | - instantiate 627 | 628 | - add options to `parser` attribute (an instance of `FuseOptParse`) 629 | 630 | - call `parse` 631 | 632 | - call `main` 633 | """ 634 | 635 | _attrs = ['getattr', 'readlink', 'readdir', 'mknod', 'mkdir', 636 | 'unlink', 'rmdir', 'symlink', 'rename', 'link', 'chmod', 637 | 'chown', 'truncate', 'utime', 'open', 'read', 'write', 'release', 638 | 'statfs', 'fsync', 'create', 'opendir', 'releasedir', 'fsyncdir', 639 | 'flush', 'fgetattr', 'ftruncate', 'getxattr', 'listxattr', 640 | 'setxattr', 'removexattr', 'access', 'lock', 'utimens', 'bmap', 641 | 'fsinit', 'fsdestroy'] 642 | 643 | fusage = "%prog [mountpoint] [options]" 644 | 645 | def __init__(self, *args, **kw): 646 | """ 647 | Not much happens here apart from initializing the `parser` attribute. 648 | Arguments are forwarded to the constructor of the parser class almost 649 | unchanged. 650 | 651 | The parser class is `FuseOptParse` unless you specify one using the 652 | ``parser_class`` keyword. (See `FuseOptParse` documentation for 653 | available options.) 654 | """ 655 | 656 | if not fuse_python_api: 657 | raise RuntimeError, __name__ + """.fuse_python_api not defined. 658 | 659 | ! Please define """ + __name__ + """.fuse_python_api internally (eg. 660 | ! 661 | ! (1) """ + __name__ + """.fuse_python_api = """ + `FUSE_PYTHON_API_VERSION` + """ 662 | ! 663 | ! ) or in the enviroment (eg. 664 | ! 665 | ! (2) FUSE_PYTHON_API=0.1 666 | ! 667 | ! ). 668 | ! 669 | ! If you are actually developing a filesystem, probably (1) is the way to go. 670 | ! If you are using a filesystem written before 2007 Q2, probably (2) is what 671 | ! you want." 672 | """ 673 | 674 | def malformed(): 675 | raise RuntimeError, \ 676 | "malformatted fuse_python_api value " + `fuse_python_api` 677 | 678 | if not isinstance(fuse_python_api, tuple): 679 | malformed() 680 | for i in fuse_python_api: 681 | if not isinstance(i, int) or i < 0: 682 | malformed() 683 | 684 | if fuse_python_api > FUSE_PYTHON_API_VERSION: 685 | raise RuntimeError, """ 686 | ! You require FUSE-Python API version """ + `fuse_python_api` + """. 687 | ! However, the latest available is """ + `FUSE_PYTHON_API_VERSION` + """. 688 | """ 689 | 690 | self.fuse_args = \ 691 | 'fuse_args' in kw and kw.pop('fuse_args') or FuseArgs() 692 | 693 | if get_compat_0_1(): 694 | return self.__init_0_1__(*args, **kw) 695 | 696 | self.multithreaded = True 697 | 698 | if not 'usage' in kw: 699 | kw['usage'] = self.fusage 700 | if not 'fuse_args' in kw: 701 | kw['fuse_args'] = self.fuse_args 702 | kw['fuse'] = self 703 | parserclass = \ 704 | 'parser_class' in kw and kw.pop('parser_class') or FuseOptParse 705 | 706 | self.parser = parserclass(*args, **kw) 707 | self.methproxy = self.Methproxy() 708 | 709 | def parse(self, *args, **kw): 710 | """Parse command line, fill `fuse_args` attribute.""" 711 | 712 | ev = 'errex' in kw and kw.pop('errex') 713 | if ev and not isinstance(ev, int): 714 | raise TypeError, "error exit value should be an integer" 715 | 716 | try: 717 | self.cmdline = self.parser.parse_args(*args, **kw) 718 | except OptParseError: 719 | if ev: 720 | sys.exit(ev) 721 | raise 722 | 723 | return self.fuse_args 724 | 725 | def main(self, args=None): 726 | """Enter filesystem service loop.""" 727 | 728 | if get_compat_0_1(): 729 | args = self.main_0_1_preamble() 730 | 731 | d = {'multithreaded': self.multithreaded and 1 or 0} 732 | d['fuse_args'] = args or self.fuse_args.assemble() 733 | 734 | for t in 'file_class', 'dir_class': 735 | if hasattr(self, t): 736 | getattr(self.methproxy, 'set_' + t)(getattr(self, t)) 737 | 738 | for a in self._attrs: 739 | b = a 740 | if get_compat_0_1() and a in self.compatmap: 741 | b = self.compatmap[a] 742 | if hasattr(self, b): 743 | c = '' 744 | if get_compat_0_1() and hasattr(self, a + '_compat_0_1'): 745 | c = '_compat_0_1' 746 | d[a] = ErrnoWrapper(self.lowwrap(a + c)) 747 | 748 | try: 749 | main(**d) 750 | except FuseError: 751 | if args or self.fuse_args.mount_expected(): 752 | raise 753 | 754 | def lowwrap(self, fname): 755 | """ 756 | Wraps the fname method when the C code expects a different kind of 757 | callback than we have in the fusepy API. (The wrapper is usually for 758 | performing some checks or transfromations which could be done in C but 759 | is simpler if done in Python.) 760 | 761 | Currently `open` and `create` are wrapped: a boolean flag is added 762 | which indicates if the result is to be kept during the opened file's 763 | lifetime or can be thrown away. Namely, it's considered disposable 764 | if it's an instance of FuseFileInfo. 765 | """ 766 | fun = getattr(self, fname) 767 | 768 | if fname in ('open', 'create'): 769 | def wrap(*a, **kw): 770 | res = fun(*a, **kw) 771 | if not res or type(res) == type(0): 772 | return res 773 | else: 774 | return (res, type(res) != FuseFileInfo) 775 | elif fname == 'utimens': 776 | def wrap(path, acc_sec, acc_nsec, mod_sec, mod_nsec): 777 | ts_acc = Timespec(tv_sec=acc_sec, tv_nsec=acc_nsec) 778 | ts_mod = Timespec(tv_sec=mod_sec, tv_nsec=mod_nsec) 779 | return fun(path, ts_acc, ts_mod) 780 | else: 781 | wrap = fun 782 | 783 | return wrap 784 | 785 | def GetContext(self): 786 | return FuseGetContext(self) 787 | 788 | def Invalidate(self, path): 789 | return FuseInvalidate(self, path) 790 | 791 | def fuseoptref(cls): 792 | """ 793 | Find out which options are recognized by the library. 794 | Result is a `FuseArgs` instance with the list of supported 795 | options, suitable for passing on to the `filter` method of 796 | another `FuseArgs` instance. 797 | """ 798 | 799 | import os, re 800 | 801 | pr, pw = os.pipe() 802 | pid = os.fork() 803 | if pid == 0: 804 | os.dup2(pw, 2) 805 | os.close(pr) 806 | 807 | fh = cls() 808 | fh.fuse_args = FuseArgs() 809 | fh.fuse_args.setmod('showhelp') 810 | fh.main() 811 | sys.exit() 812 | 813 | os.close(pw) 814 | 815 | fa = FuseArgs() 816 | ore = re.compile("-o\s+([\w\[\]]+(?:=\w+)?)") 817 | fpr = os.fdopen(pr) 818 | for l in fpr: 819 | m = ore.search(l) 820 | if m: 821 | o = m.groups()[0] 822 | oa = [o] 823 | # try to catch two-in-one options (like "[no]foo") 824 | opa = o.split("[") 825 | if len(opa) == 2: 826 | o1, ox = opa 827 | oxpa = ox.split("]") 828 | if len(oxpa) == 2: 829 | oo, o2 = oxpa 830 | oa = [o1 + o2, o1 + oo + o2] 831 | for o in oa: 832 | fa.add(o) 833 | 834 | fpr.close() 835 | return fa 836 | 837 | fuseoptref = classmethod(fuseoptref) 838 | 839 | class Methproxy(object): 840 | 841 | def __init__(self): 842 | 843 | class mpx(object): 844 | def __init__(self, name): 845 | self.name = name 846 | 847 | def __call__(self, *a, **kw): 848 | return getattr(a[-1], self.name)(*(a[1:-1]), **kw) 849 | 850 | self.proxyclass = mpx 851 | self.mdic = {} 852 | self.file_class = None 853 | self.dir_class = None 854 | 855 | def __call__(self, meth): 856 | return meth in self.mdic and self.mdic[meth] or None 857 | 858 | def _add_class_type(cls, type, inits, proxied): 859 | 860 | def setter(self, xcls): 861 | 862 | setattr(self, type + '_class', xcls) 863 | 864 | for m in inits: 865 | self.mdic[m] = xcls 866 | 867 | for m in proxied: 868 | if hasattr(xcls, m): 869 | self.mdic[m] = self.proxyclass(m) 870 | 871 | setattr(cls, 'set_' + type + '_class', setter) 872 | 873 | _add_class_type = classmethod(_add_class_type) 874 | 875 | Methproxy._add_class_type('file', ('open', 'create'), 876 | ('read', 'write', 'fsync', 'release', 'flush', 877 | 'fgetattr', 'ftruncate', 'lock')) 878 | Methproxy._add_class_type('dir', ('opendir',), 879 | ('readdir', 'fsyncdir', 'releasedir')) 880 | 881 | def __getattr__(self, meth): 882 | 883 | m = self.methproxy(meth) 884 | if m: 885 | return m 886 | 887 | raise AttributeError, "Fuse instance has no attribute '%s'" % meth 888 | 889 | ########## 890 | ### 891 | ### Compat stuff. 892 | ### 893 | ########## 894 | 895 | def __init_0_1__(self, *args, **kw): 896 | 897 | self.flags = 0 898 | multithreaded = 0 899 | 900 | # default attributes 901 | if args == (): 902 | # there is a self.optlist.append() later on, make sure it won't 903 | # bomb out. 904 | self.optlist = [] 905 | else: 906 | self.optlist = args 907 | self.optdict = kw 908 | 909 | if len(self.optlist) == 1: 910 | self.mountpoint = self.optlist[0] 911 | else: 912 | self.mountpoint = None 913 | 914 | # grab command-line arguments, if any. 915 | # Those will override whatever parameters 916 | # were passed to __init__ directly. 917 | argv = sys.argv 918 | argc = len(argv) 919 | if argc > 1: 920 | # we've been given the mountpoint 921 | self.mountpoint = argv[1] 922 | if argc > 2: 923 | # we've received mount args 924 | optstr = argv[2] 925 | opts = optstr.split(",") 926 | for o in opts: 927 | try: 928 | k, v = o.split("=", 1) 929 | self.optdict[k] = v 930 | except: 931 | self.optlist.append(o) 932 | 933 | def main_0_1_preamble(self): 934 | 935 | cfargs = FuseArgs() 936 | 937 | cfargs.mountpoint = self.mountpoint 938 | 939 | if hasattr(self, 'debug'): 940 | cfargs.add('debug') 941 | 942 | if hasattr(self, 'allow_other'): 943 | cfargs.add('allow_other') 944 | 945 | if hasattr(self, 'kernel_cache'): 946 | cfargs.add('kernel_cache') 947 | 948 | return cfargs.assemble() 949 | 950 | def getattr_compat_0_1(self, *a): 951 | from os import stat_result 952 | 953 | return stat_result(self.getattr(*a)) 954 | 955 | def statfs_compat_0_1(self, *a): 956 | 957 | oout = self.statfs(*a) 958 | lo = len(oout) 959 | 960 | svf = StatVfs() 961 | svf.f_bsize = oout[0] # 0 962 | svf.f_frsize = oout[lo >= 8 and 7 or 0] # 1 963 | svf.f_blocks = oout[1] # 2 964 | svf.f_bfree = oout[2] # 3 965 | svf.f_bavail = oout[3] # 4 966 | svf.f_files = oout[4] # 5 967 | svf.f_ffree = oout[5] # 6 968 | svf.f_favail = lo >= 9 and oout[8] or 0 # 7 969 | svf.f_flag = lo >= 10 and oout[9] or 0 # 8 970 | svf.f_namemax = oout[6] # 9 971 | 972 | return svf 973 | 974 | def readdir_compat_0_1(self, path, offset, *fh): 975 | 976 | for name, type in self.getdir(path): 977 | de = Direntry(name) 978 | de.type = type 979 | 980 | yield de 981 | 982 | compatmap = {'readdir': 'getdir'} -------------------------------------------------------------------------------- /dedupfs/get_memory_usage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | """ 4 | The function in this Python module determines the current memory usage of the 5 | current process by reading the VmSize value from /proc/$pid/status. It's based 6 | on the following entry in the Python cookbook: 7 | http://code.activestate.com/recipes/286222/ 8 | """ 9 | 10 | import os 11 | 12 | _units = { 'KB': 1024, 'MB': 1024**2, 'GB': 1024**3 } 13 | _handle = _handle = open('/proc/%d/status' % os.getpid()) 14 | 15 | def get_memory_usage(): 16 | global _proc_status, _units, _handle 17 | try: 18 | for line in _handle: 19 | if line.startswith('VmSize:'): 20 | label, count, unit = line.split() 21 | return int(count) * _units[unit.upper()] 22 | except: 23 | return 0 24 | finally: 25 | _handle.seek(0) 26 | 27 | if __name__ == '__main__': 28 | from my_formats import format_size 29 | megabyte = 1024**2 30 | counter = megabyte 31 | limit = megabyte * 50 32 | memory = [] 33 | old_memory_usage = get_memory_usage() 34 | assert old_memory_usage > 0 35 | while counter < limit: 36 | memory.append('a' * counter) 37 | msg = "I've just allocated %s and get_memory_usage() returns %s (%s more, deviation is %s)" 38 | new_memory_usage = get_memory_usage() 39 | difference = new_memory_usage - old_memory_usage 40 | deviation = max(difference, counter) - min(difference, counter) 41 | assert deviation < 1024*100 42 | print msg % (format_size(counter), format_size(new_memory_usage), format_size(difference), format_size(deviation)) 43 | old_memory_usage = new_memory_usage 44 | counter += megabyte 45 | print "Stopped allocating new strings at %s" % format_size(limit) 46 | 47 | # vim: ts=2 sw=2 et 48 | -------------------------------------------------------------------------------- /dedupfs/lzo/README: -------------------------------------------------------------------------------- 1 | This is a simple binding to the canonical LZO compression library. To compile 2 | first make sure you have both liblzo2 and the headers installed (on Ubuntu you 3 | can install the packages `liblzo2' and `liblzo2-dev'), then run the command 4 | `python setup.py build && python setup.py install'. 5 | 6 | Please note that this is my first Python/C interfacing code so be careful :-) 7 | 8 | - Peter Odding 9 | -------------------------------------------------------------------------------- /dedupfs/lzo/lzomodule.c: -------------------------------------------------------------------------------- 1 | #define BLOCK_SIZE (1024 * 128) 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | /* The following formula gives the worst possible compressed size. */ 8 | #define lzo1x_worst_compress(x) ((x) + ((x) / 16) + 64 + 3) 9 | 10 | /* The size of and pointer to the shared buffer. */ 11 | static int block_size, buffer_size = 0; 12 | static unsigned char *shared_buffer = NULL; 13 | 14 | /* Don't store the size of compressed blocks in headers and trust the user to 15 | * configure the correct block size? */ 16 | static int omit_headers = 0; 17 | 18 | /* Working memory required by LZO library, allocated on first use. */ 19 | static char *working_memory = NULL; 20 | 21 | #define ADD_SIZE(p) (omit_headers ? (p) : ((p) + sizeof(int))) 22 | #define SUB_SIZE(p) (omit_headers ? (p) : ((p) - sizeof(int))) 23 | 24 | static unsigned char * 25 | get_buffer(int length) 26 | { 27 | if (omit_headers) { 28 | if (!shared_buffer) 29 | shared_buffer = malloc(buffer_size); 30 | } else if (!shared_buffer || buffer_size < length) { 31 | free(shared_buffer); 32 | shared_buffer = malloc(length); 33 | buffer_size = length; 34 | } 35 | return shared_buffer; 36 | } 37 | 38 | static PyObject * 39 | set_block_size(PyObject *self, PyObject *args) 40 | { 41 | int new_block_size; 42 | 43 | if (PyArg_ParseTuple(args, "i", &new_block_size)) { 44 | if (shared_buffer) 45 | free(shared_buffer); 46 | block_size = new_block_size; 47 | buffer_size = lzo1x_worst_compress(block_size); 48 | shared_buffer = malloc(buffer_size); 49 | omit_headers = 1; 50 | } 51 | 52 | Py_INCREF(Py_True); 53 | return Py_True; 54 | } 55 | 56 | static PyObject * 57 | lzo_compress(PyObject *self, PyObject *args) 58 | { 59 | const unsigned char *input; 60 | unsigned char *output; 61 | unsigned int inlen, status; 62 | lzo_uint outlen; 63 | 64 | /* Get the uncompressed string and its length. */ 65 | if (!PyArg_ParseTuple(args, "s#", &input, &inlen)) 66 | return NULL; 67 | 68 | /* Make sure never to touch unallocated memory. */ 69 | if (omit_headers && inlen > block_size) 70 | return PyErr_Format(PyExc_ValueError, "The given input of %i bytes is larger than the configured block size of %i bytes!", block_size, inlen); 71 | 72 | /* Allocate the working memory on first use? */ 73 | if (!working_memory && !(working_memory = malloc(LZO1X_999_MEM_COMPRESS))) 74 | return PyErr_NoMemory(); 75 | 76 | /* Allocate the output buffer. */ 77 | outlen = lzo1x_worst_compress(inlen); 78 | output = get_buffer(ADD_SIZE(outlen)); 79 | if (!output) 80 | return PyErr_NoMemory(); 81 | 82 | /* Store the input size in the header of the compressed block? */ 83 | if (!omit_headers) 84 | *((int*)output) = inlen; 85 | 86 | /* Compress the input string. The default LZO compression function is 87 | * lzo1x_1_compress(). There's also variants like lzo1x_1_15_compress() which 88 | * is faster and lzo1x_999_compress() which achieves higher compression. */ 89 | status = lzo1x_1_15_compress(input, inlen, ADD_SIZE(output), &outlen, working_memory); 90 | if (status != LZO_E_OK) 91 | return PyErr_Format(PyExc_Exception, "lzo_compress() failed with error code %i!", status); 92 | 93 | /* Return the compressed string. */ 94 | return Py_BuildValue("s#", output, ADD_SIZE(outlen)); 95 | } 96 | 97 | static PyObject * 98 | lzo_decompress(PyObject *self, PyObject *args) 99 | { 100 | const unsigned char *input; 101 | unsigned char *output; 102 | int inlen, outlen_expected = 0, status; 103 | lzo_uint outlen_actual; 104 | 105 | /* Get the compressed string and its length. */ 106 | if (!PyArg_ParseTuple(args, "s#", &input, &inlen)) 107 | return NULL; 108 | 109 | /* Get the length of the uncompressed string? */ 110 | if (!omit_headers) 111 | outlen_expected = *((int*)input); 112 | 113 | /* Allocate the output buffer. */ 114 | output = get_buffer(outlen_expected); 115 | if (!output) 116 | return PyErr_NoMemory(); 117 | 118 | /* Decompress the compressed string. */ 119 | status = lzo1x_decompress(ADD_SIZE(input), SUB_SIZE(inlen), output, &outlen_actual, NULL); 120 | if (status != LZO_E_OK) 121 | return PyErr_Format(PyExc_Exception, "lzo_decompress() failed with error code %i!", status); 122 | 123 | /* Verify the length of the uncompressed data? */ 124 | if (!omit_headers && outlen_expected != outlen_actual) 125 | return PyErr_Format(PyExc_Exception, "The expected length (%i) doesn't match the actual uncompressed length (%i)!", outlen_expected, (int)outlen_actual); 126 | 127 | /* Return the decompressed string. */ 128 | return Py_BuildValue("s#", output, (int)outlen_actual); 129 | } 130 | 131 | static PyMethodDef functions[] = { 132 | { "compress", lzo_compress, METH_VARARGS, "Compress a string using the LZO algorithm." }, 133 | { "decompress", lzo_decompress, METH_VARARGS, "Decompress a string that was previously compressed using the compress() function of this same module." }, 134 | { "set_block_size", set_block_size, METH_VARARGS, "Set the max. length of the strings you will be compressing and/or decompressing so that the LZO module can allocate a single buffer shared by all lzo.compress() and lzo.decompress() calls." }, 135 | { NULL, NULL, 0, NULL } 136 | }; 137 | 138 | PyMODINIT_FUNC 139 | initlzo(void) 140 | { 141 | int status; 142 | 143 | if ((status = lzo_init()) != LZO_E_OK) 144 | PyErr_Format(PyExc_Exception, "Failed to initialize the LZO library! (lzo_init() failed with error code %i)", status); 145 | else if (!Py_InitModule("lzo", functions)) 146 | PyErr_Format(PyExc_Exception, "Failed to register module functions!"); 147 | } 148 | 149 | /* vim: set ts=2 sw=2 et : */ 150 | -------------------------------------------------------------------------------- /dedupfs/lzo/setup.py: -------------------------------------------------------------------------------- 1 | from distutils.core import setup, Extension 2 | 3 | setup(name = "LZO", version = "1.0", 4 | ext_modules = [Extension("lzo", ["lzomodule.c"], libraries=['lzo2'])]) 5 | -------------------------------------------------------------------------------- /dedupfs/my_formats.py: -------------------------------------------------------------------------------- 1 | from math import floor 2 | 3 | def format_timespan(seconds): # {{{1 4 | """ 5 | Format a timespan in seconds as a human-readable string. 6 | """ 7 | result = [] 8 | units = [('day', 60 * 60 * 24), ('hour', 60 * 60), ('minute', 60), ('second', 1)] 9 | for name, size in units: 10 | if seconds >= size: 11 | count = seconds / size 12 | seconds %= size 13 | result.append('%i %s%s' % (count, name, floor(count) != 1 and 's' or '')) 14 | if result == []: 15 | return 'less than a second' 16 | if len(result) == 1: 17 | return result[0] 18 | else: 19 | return ', '.join(result[:-1]) + ' and ' + result[-1] 20 | 21 | def format_size(nbytes): 22 | """ 23 | Format a byte count as a human-readable file size. 24 | """ 25 | return nbytes < 1024 and '%i bytes' % nbytes \ 26 | or nbytes < (1024 ** 2) and __round(nbytes, 1024, 'KB') \ 27 | or nbytes < (1024 ** 3) and __round(nbytes, 1024 ** 2, 'MB') \ 28 | or nbytes < (1024 ** 4) and __round(nbytes, 1024 ** 3, 'GB') \ 29 | or __round(nbytes, 1024 ** 4, 'TB') 30 | 31 | def __round(nbytes, divisor, suffix): 32 | nbytes = float(nbytes) / divisor 33 | if floor(nbytes) == nbytes: 34 | return str(int(nbytes)) + ' ' + suffix 35 | else: 36 | return '%.2f %s' % (nbytes, suffix) 37 | 38 | # vim: sw=2 sw=2 et 39 | -------------------------------------------------------------------------------- /dedupfs/tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | TIMESTAMP="`date +%s`" 4 | ROOTDIR="/tmp/dedupfs-tests-$TIMESTAMP" 5 | MOUNTPOINT="$ROOTDIR/mountpoint" 6 | METASTORE="$ROOTDIR/metastore.sqlite3" 7 | DATASTORE="$ROOTDIR/datastore.db" 8 | WAITTIME=1 9 | TESTNO=1 10 | 11 | # Initialization. {{{1 12 | 13 | FAIL () { 14 | FAIL_INTERNAL "$@" 15 | exit 1 16 | } 17 | 18 | MESSAGE () { 19 | tput bold 20 | echo "$@" >&2 21 | tput sgr0 22 | } 23 | 24 | FAIL_INTERNAL () { 25 | echo -ne '\033[31m' >&2 26 | MESSAGE "$@" 27 | echo -ne '\033[0m' >&2 28 | } 29 | 30 | CLEANUP () { 31 | DO_UNMOUNT 32 | if ! rm -R "$ROOTDIR"; then 33 | FAIL_INTERNAL "$0:$LINENO: Failed to delete temporary directory!" 34 | fi 35 | } 36 | 37 | # Create the root and mount directories. 38 | mkdir -p "$MOUNTPOINT" 39 | if [ ! -d "$MOUNTPOINT" ]; then 40 | FAIL "$0:$LINENO: Failed to create mount directory $MOUNTPOINT!" 41 | exit 1 42 | fi 43 | 44 | DO_MOUNT () { 45 | # Mount the file system using the two temporary databases. 46 | python dedupfs.py -fv "$@" "--metastore=$METASTORE" "--datastore=$DATASTORE" "$MOUNTPOINT" & 47 | # Wait a while before accessing the mount point, to 48 | # make sure the file system has been fully initialized. 49 | while true; do 50 | sleep $WAITTIME 51 | if mount | grep -q "$MOUNTPOINT"; then break; fi 52 | done 53 | } 54 | 55 | DO_UNMOUNT () { 56 | if mount | grep -q "$MOUNTPOINT"; then 57 | sleep $WAITTIME 58 | if ! fusermount -u "$MOUNTPOINT"; then 59 | FAIL_INTERNAL "$0:$LINENO: Failed to unmount the mount point?!" 60 | fi 61 | while true; do 62 | sleep $WAITTIME 63 | if ! mount | grep -q "$MOUNTPOINT"; then break; fi 64 | done 65 | fi 66 | } 67 | 68 | DO_MOUNT --verify-writes --compress=lzo 69 | 70 | # Tests 1-8: Test hard link counts with mkdir(), rmdir() and rename(). {{{1 71 | 72 | CHECK_NLINK () { 73 | NLINK=`ls -ld "$1" | awk '{print $2}'` 74 | [ $NLINK -eq $2 ] || FAIL "$0:$3: Expected link count of $1 to be $2, got $NLINK!" 75 | } 76 | 77 | FEEDBACK () { 78 | MESSAGE "Running test $1" 79 | } 80 | 81 | # Test 1: Check link count of file system root. {{{2 82 | 83 | FEEDBACK $TESTNO 84 | TESTNO=$[$TESTNO + 1] 85 | CHECK_NLINK "$MOUNTPOINT" 2 $LINENO 86 | 87 | # Test 2: Check link count of newly created file. {{{2 88 | 89 | FEEDBACK $TESTNO 90 | TESTNO=$[$TESTNO + 1] 91 | FILE="$MOUNTPOINT/file_nlink_test" 92 | touch "$FILE" 93 | CHECK_NLINK "$FILE" 1 $LINENO 94 | CHECK_NLINK "$MOUNTPOINT" 2 $LINENO 95 | 96 | # Test 3: Check link count of hard link to existing file. {{{2 97 | 98 | FEEDBACK $TESTNO 99 | TESTNO=$[$TESTNO + 1] 100 | 101 | LINK="$MOUNTPOINT/link_to_file" 102 | link "$FILE" "$LINK" 103 | CHECK_NLINK "$FILE" 2 $LINENO 104 | CHECK_NLINK "$LINK" 2 $LINENO 105 | CHECK_NLINK "$MOUNTPOINT" 2 $LINENO 106 | unlink "$LINK" 107 | CHECK_NLINK "$FILE" 2 $LINENO 108 | CHECK_NLINK "$MOUNTPOINT" 2 $LINENO 109 | 110 | # Test 4: Check link count of newly created subdirectory. {{{2 111 | 112 | FEEDBACK $TESTNO 113 | TESTNO=$[$TESTNO + 1] 114 | 115 | SUBDIR="$MOUNTPOINT/dir1" 116 | mkdir "$SUBDIR" 117 | if [ ! -d "$SUBDIR" ]; then 118 | FAIL "$0:$LINENO: Failed to create subdirectory $SUBDIR!" 119 | fi 120 | 121 | CHECK_NLINK "$SUBDIR" 2 $LINENO 122 | 123 | # Test 5: Check that nlink of root is incremented by one (because of subdirectory created above). {{{2 124 | 125 | FEEDBACK $TESTNO 126 | TESTNO=$[$TESTNO + 1] 127 | 128 | CHECK_NLINK "$MOUNTPOINT" 3 $LINENO 129 | 130 | # Test 6: Check that non-empty directories cannot be removed with rmdir(). {{{2 131 | 132 | FEEDBACK $TESTNO 133 | TESTNO=$[$TESTNO + 1] 134 | 135 | SUBFILE="$SUBDIR/file" 136 | touch "$SUBFILE" 137 | if rmdir "$SUBDIR" 2>/dev/null; then 138 | FAIL "$0:$LINENO: rmdir() didn't fail when deleting a non-empty directory!" 139 | elif ! rm -R "$SUBDIR"; then 140 | FAIL "$0:$LINENO: Failed to recursively delete directory?!" 141 | fi 142 | 143 | # Test 7: Check that link count of root is decremented by one (because of subdirectory deleted above). {{{2 144 | 145 | FEEDBACK $TESTNO 146 | TESTNO=$[$TESTNO + 1] 147 | 148 | CHECK_NLINK "$MOUNTPOINT" 2 $LINENO 149 | 150 | # Test 8: Check that link counts are updated when directories are renamed. {{{2 151 | 152 | FEEDBACK $TESTNO 153 | TESTNO=$[$TESTNO + 1] 154 | 155 | ORIGDIR="$MOUNTPOINT/original-directory" 156 | REPLDIR="$MOUNTPOINT/replacement-directory" 157 | mkdir -p "$ORIGDIR/subdir" "$REPLDIR/subdir" 158 | for DIRNAME in "$ORIGDIR" "$REPLDIR"; do CHECK_NLINK "$DIRNAME" 3 $LINENO; done 159 | mv -T "$ORIGDIR/subdir" "$REPLDIR/subdir" 160 | CHECK_NLINK "$ORIGDIR" 2 $LINENO 161 | CHECK_NLINK "$REPLDIR" 3 $LINENO 162 | 163 | # Tests 9-14: Write random binary data to file system and verify that it reads back unchanged. {{{1 164 | 165 | TESTDATA="$ROOTDIR/testdata" 166 | 167 | WRITE_TESTNO=0 168 | while [ $WRITE_TESTNO -le 5 ]; do 169 | FEEDBACK $TESTNO 170 | TESTNO=$[$TESTNO + 1] 171 | NBYTES=$[$RANDOM % (1024 * 257)] 172 | head -c $NBYTES /dev/urandom > "$TESTDATA" 173 | WRITE_FILE="$MOUNTPOINT/$RANDOM" 174 | cp -a "$TESTDATA" "$WRITE_FILE" 175 | sleep $WAITTIME 176 | if ! cmp -s "$TESTDATA" "$WRITE_FILE"; then 177 | (sleep 1 178 | echo "Differences:" 179 | ls -l "$TESTDATA" "$WRITE_FILE" 180 | cmp -lb "$TESTDATA" "$WRITE_FILE") & 181 | FAIL "$0:$LINENO: Failed to verify $WRITE_FILE of $NBYTES bytes!" 182 | fi 183 | WRITE_TESTNO=$[$WRITE_TESTNO + 1] 184 | done 185 | 186 | # Test 15: Verify that written data persists when remounted. {{{1 187 | 188 | FEEDBACK $TESTNO 189 | TESTNO=$[$TESTNO + 1] 190 | 191 | DO_UNMOUNT 192 | DO_MOUNT --nogc # <- important for the following tests. 193 | if ! cmp -s "$TESTDATA" "$WRITE_FILE"; then 194 | (sleep 1 195 | echo "Differences:" 196 | ls -l "$TESTDATA" "$WRITE_FILE" 197 | cmp -lb "$TESTDATA" "$WRITE_FILE") & 198 | FAIL "$0:$LINENO: Failed to verify $WRITE_FILE of $NBYTES bytes!" 199 | fi 200 | 201 | # Test 16: Verify that garbage collection of unused data blocks works. {{{1 202 | 203 | FEEDBACK $TESTNO 204 | TESTNO=$[$TESTNO + 1] 205 | 206 | DO_UNMOUNT 207 | FULL_SIZE=`ls -l "$DATASTORE" | awk '{print $5}'` 208 | HALF_SIZE=$[$FULL_SIZE / 2] 209 | DO_MOUNT 210 | rm $MOUNTPOINT/* 2>/dev/null 211 | DO_UNMOUNT 212 | REDUCED_SIZE=`ls -l "$DATASTORE" | awk '{print $5}'` 213 | [ $REDUCED_SIZE -lt $HALF_SIZE ] || FAIL "$0:$LINENO: Failed to verify effectiveness of data block garbage collection! (Full size of data store: $FULL_SIZE, reduced size: $REDUCED_SIZE)" 214 | 215 | # Test 17: Verify that garbage collection of interned path segments works. {{{1 216 | 217 | DO_MOUNT --nosync 218 | SEGMENTGCDIR="$MOUNTPOINT/gc-of-segments-test" 219 | mkdir "$SEGMENTGCDIR" 220 | for ((i=0;i<=512;i+=1)); do 221 | echo -ne "\rCreating segment $i" 222 | touch "$SEGMENTGCDIR/$i" 223 | done 224 | echo -ne "\rSyncing to disk using unmount" 225 | DO_UNMOUNT 226 | FULL_SIZE=`ls -l "$METASTORE" | awk '{print $5}'` 227 | HALF_SIZE=$[$FULL_SIZE / 2] 228 | echo -ne "\rDeleting segments" 229 | DO_MOUNT --nosync 230 | rm -R "$SEGMENTGCDIR" 231 | echo -ne "\rSyncing to disk using unmount" 232 | DO_UNMOUNT 233 | REDUCED_SIZE=`ls -l "$METASTORE" | awk '{print $5}'` 234 | [ $REDUCED_SIZE -lt $HALF_SIZE ] || FAIL "$0:$LINENO: Failed to verify effectiveness of interned string garbage collection! (Full size of metadata store: $FULL_SIZE, reduced size: $REDUCED_SIZE)" 235 | echo -ne "\r" 236 | 237 | # Finalization. {{{1 238 | 239 | CLEANUP 240 | MESSAGE "All tests passed!" 241 | 242 | # vim: ts=2 sw=2 et 243 | -------------------------------------------------------------------------------- /doc/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Opensourse Telegram based cloud storage 12 | 13 | 14 | 15 |
16 | 17 |

Opensourse Telegram based cloud storage

18 | 19 |

Описание концепции

20 | 21 |
    22 |
  • Хранилище централизованное
  • 23 |
  • Для доступа организовывается канал и группа в Telegram
  • 24 |
  • Канал является основным хранилищем файлов
  • 25 |
  • Группа является управляющим каналом между клиентской и серверной частью
  • 26 |
  • Серверная часть работает на VPS в виде отдельного приложения. Серверная часть имеет права админа в канал и занимается управлением файлами
  • 27 |
  • Клиентская часть организует веб-интерфейс для доступа к файлам
  • 28 |
29 | 30 |
31 | 32 | 33 | -------------------------------------------------------------------------------- /doc/static/css/font-awesome.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome 3 | * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) 4 | */ 5 | /* FONT PATH 6 | * -------------------------- */ 7 | @font-face { 8 | font-family: 'FontAwesome'; 9 | src: url('../fonts/fontawesome-webfont.eot?v=4.7.0'); 10 | src: url('../fonts/fontawesome-webfont.eot?#iefix&v=4.7.0') format('embedded-opentype'), url('../fonts/fontawesome-webfont.woff2?v=4.7.0') format('woff2'), url('../fonts/fontawesome-webfont.woff?v=4.7.0') format('woff'), url('../fonts/fontawesome-webfont.ttf?v=4.7.0') format('truetype'), url('../fonts/fontawesome-webfont.svg?v=4.7.0#fontawesomeregular') format('svg'); 11 | font-weight: normal; 12 | font-style: normal; 13 | } 14 | .fa { 15 | display: inline-block; 16 | font: normal normal normal 14px/1 FontAwesome; 17 | font-size: inherit; 18 | text-rendering: auto; 19 | -webkit-font-smoothing: antialiased; 20 | -moz-osx-font-smoothing: grayscale; 21 | } 22 | /* makes the font 33% larger relative to the icon container */ 23 | .fa-lg { 24 | font-size: 1.33333333em; 25 | line-height: 0.75em; 26 | vertical-align: -15%; 27 | } 28 | .fa-2x { 29 | font-size: 2em; 30 | } 31 | .fa-3x { 32 | font-size: 3em; 33 | } 34 | .fa-4x { 35 | font-size: 4em; 36 | } 37 | .fa-5x { 38 | font-size: 5em; 39 | } 40 | .fa-fw { 41 | width: 1.28571429em; 42 | text-align: center; 43 | } 44 | .fa-ul { 45 | padding-left: 0; 46 | margin-left: 2.14285714em; 47 | list-style-type: none; 48 | } 49 | .fa-ul > li { 50 | position: relative; 51 | } 52 | .fa-li { 53 | position: absolute; 54 | left: -2.14285714em; 55 | width: 2.14285714em; 56 | top: 0.14285714em; 57 | text-align: center; 58 | } 59 | .fa-li.fa-lg { 60 | left: -1.85714286em; 61 | } 62 | .fa-border { 63 | padding: .2em .25em .15em; 64 | border: solid 0.08em #eeeeee; 65 | border-radius: .1em; 66 | } 67 | .fa-pull-left { 68 | float: left; 69 | } 70 | .fa-pull-right { 71 | float: right; 72 | } 73 | .fa.fa-pull-left { 74 | margin-right: .3em; 75 | } 76 | .fa.fa-pull-right { 77 | margin-left: .3em; 78 | } 79 | /* Deprecated as of 4.4.0 */ 80 | .pull-right { 81 | float: right; 82 | } 83 | .pull-left { 84 | float: left; 85 | } 86 | .fa.pull-left { 87 | margin-right: .3em; 88 | } 89 | .fa.pull-right { 90 | margin-left: .3em; 91 | } 92 | .fa-spin { 93 | -webkit-animation: fa-spin 2s infinite linear; 94 | animation: fa-spin 2s infinite linear; 95 | } 96 | .fa-pulse { 97 | -webkit-animation: fa-spin 1s infinite steps(8); 98 | animation: fa-spin 1s infinite steps(8); 99 | } 100 | @-webkit-keyframes fa-spin { 101 | 0% { 102 | -webkit-transform: rotate(0deg); 103 | transform: rotate(0deg); 104 | } 105 | 100% { 106 | -webkit-transform: rotate(359deg); 107 | transform: rotate(359deg); 108 | } 109 | } 110 | @keyframes fa-spin { 111 | 0% { 112 | -webkit-transform: rotate(0deg); 113 | transform: rotate(0deg); 114 | } 115 | 100% { 116 | -webkit-transform: rotate(359deg); 117 | transform: rotate(359deg); 118 | } 119 | } 120 | .fa-rotate-90 { 121 | -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=1)"; 122 | -webkit-transform: rotate(90deg); 123 | -ms-transform: rotate(90deg); 124 | transform: rotate(90deg); 125 | } 126 | .fa-rotate-180 { 127 | -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=2)"; 128 | -webkit-transform: rotate(180deg); 129 | -ms-transform: rotate(180deg); 130 | transform: rotate(180deg); 131 | } 132 | .fa-rotate-270 { 133 | -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=3)"; 134 | -webkit-transform: rotate(270deg); 135 | -ms-transform: rotate(270deg); 136 | transform: rotate(270deg); 137 | } 138 | .fa-flip-horizontal { 139 | -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)"; 140 | -webkit-transform: scale(-1, 1); 141 | -ms-transform: scale(-1, 1); 142 | transform: scale(-1, 1); 143 | } 144 | .fa-flip-vertical { 145 | -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)"; 146 | -webkit-transform: scale(1, -1); 147 | -ms-transform: scale(1, -1); 148 | transform: scale(1, -1); 149 | } 150 | :root .fa-rotate-90, 151 | :root .fa-rotate-180, 152 | :root .fa-rotate-270, 153 | :root .fa-flip-horizontal, 154 | :root .fa-flip-vertical { 155 | filter: none; 156 | } 157 | .fa-stack { 158 | position: relative; 159 | display: inline-block; 160 | width: 2em; 161 | height: 2em; 162 | line-height: 2em; 163 | vertical-align: middle; 164 | } 165 | .fa-stack-1x, 166 | .fa-stack-2x { 167 | position: absolute; 168 | left: 0; 169 | width: 100%; 170 | text-align: center; 171 | } 172 | .fa-stack-1x { 173 | line-height: inherit; 174 | } 175 | .fa-stack-2x { 176 | font-size: 2em; 177 | } 178 | .fa-inverse { 179 | color: #ffffff; 180 | } 181 | /* Font Awesome uses the Unicode Private Use Area (PUA) to ensure screen 182 | readers do not read off random characters that represent icons */ 183 | .fa-glass:before { 184 | content: "\f000"; 185 | } 186 | .fa-music:before { 187 | content: "\f001"; 188 | } 189 | .fa-search:before { 190 | content: "\f002"; 191 | } 192 | .fa-envelope-o:before { 193 | content: "\f003"; 194 | } 195 | .fa-heart:before { 196 | content: "\f004"; 197 | } 198 | .fa-star:before { 199 | content: "\f005"; 200 | } 201 | .fa-star-o:before { 202 | content: "\f006"; 203 | } 204 | .fa-user:before { 205 | content: "\f007"; 206 | } 207 | .fa-film:before { 208 | content: "\f008"; 209 | } 210 | .fa-th-large:before { 211 | content: "\f009"; 212 | } 213 | .fa-th:before { 214 | content: "\f00a"; 215 | } 216 | .fa-th-list:before { 217 | content: "\f00b"; 218 | } 219 | .fa-check:before { 220 | content: "\f00c"; 221 | } 222 | .fa-remove:before, 223 | .fa-close:before, 224 | .fa-times:before { 225 | content: "\f00d"; 226 | } 227 | .fa-search-plus:before { 228 | content: "\f00e"; 229 | } 230 | .fa-search-minus:before { 231 | content: "\f010"; 232 | } 233 | .fa-power-off:before { 234 | content: "\f011"; 235 | } 236 | .fa-signal:before { 237 | content: "\f012"; 238 | } 239 | .fa-gear:before, 240 | .fa-cog:before { 241 | content: "\f013"; 242 | } 243 | .fa-trash-o:before { 244 | content: "\f014"; 245 | } 246 | .fa-home:before { 247 | content: "\f015"; 248 | } 249 | .fa-file-o:before { 250 | content: "\f016"; 251 | } 252 | .fa-clock-o:before { 253 | content: "\f017"; 254 | } 255 | .fa-road:before { 256 | content: "\f018"; 257 | } 258 | .fa-download:before { 259 | content: "\f019"; 260 | } 261 | .fa-arrow-circle-o-down:before { 262 | content: "\f01a"; 263 | } 264 | .fa-arrow-circle-o-up:before { 265 | content: "\f01b"; 266 | } 267 | .fa-inbox:before { 268 | content: "\f01c"; 269 | } 270 | .fa-play-circle-o:before { 271 | content: "\f01d"; 272 | } 273 | .fa-rotate-right:before, 274 | .fa-repeat:before { 275 | content: "\f01e"; 276 | } 277 | .fa-refresh:before { 278 | content: "\f021"; 279 | } 280 | .fa-list-alt:before { 281 | content: "\f022"; 282 | } 283 | .fa-lock:before { 284 | content: "\f023"; 285 | } 286 | .fa-flag:before { 287 | content: "\f024"; 288 | } 289 | .fa-headphones:before { 290 | content: "\f025"; 291 | } 292 | .fa-volume-off:before { 293 | content: "\f026"; 294 | } 295 | .fa-volume-down:before { 296 | content: "\f027"; 297 | } 298 | .fa-volume-up:before { 299 | content: "\f028"; 300 | } 301 | .fa-qrcode:before { 302 | content: "\f029"; 303 | } 304 | .fa-barcode:before { 305 | content: "\f02a"; 306 | } 307 | .fa-tag:before { 308 | content: "\f02b"; 309 | } 310 | .fa-tags:before { 311 | content: "\f02c"; 312 | } 313 | .fa-book:before { 314 | content: "\f02d"; 315 | } 316 | .fa-bookmark:before { 317 | content: "\f02e"; 318 | } 319 | .fa-print:before { 320 | content: "\f02f"; 321 | } 322 | .fa-camera:before { 323 | content: "\f030"; 324 | } 325 | .fa-font:before { 326 | content: "\f031"; 327 | } 328 | .fa-bold:before { 329 | content: "\f032"; 330 | } 331 | .fa-italic:before { 332 | content: "\f033"; 333 | } 334 | .fa-text-height:before { 335 | content: "\f034"; 336 | } 337 | .fa-text-width:before { 338 | content: "\f035"; 339 | } 340 | .fa-align-left:before { 341 | content: "\f036"; 342 | } 343 | .fa-align-center:before { 344 | content: "\f037"; 345 | } 346 | .fa-align-right:before { 347 | content: "\f038"; 348 | } 349 | .fa-align-justify:before { 350 | content: "\f039"; 351 | } 352 | .fa-list:before { 353 | content: "\f03a"; 354 | } 355 | .fa-dedent:before, 356 | .fa-outdent:before { 357 | content: "\f03b"; 358 | } 359 | .fa-indent:before { 360 | content: "\f03c"; 361 | } 362 | .fa-video-camera:before { 363 | content: "\f03d"; 364 | } 365 | .fa-photo:before, 366 | .fa-image:before, 367 | .fa-picture-o:before { 368 | content: "\f03e"; 369 | } 370 | .fa-pencil:before { 371 | content: "\f040"; 372 | } 373 | .fa-map-marker:before { 374 | content: "\f041"; 375 | } 376 | .fa-adjust:before { 377 | content: "\f042"; 378 | } 379 | .fa-tint:before { 380 | content: "\f043"; 381 | } 382 | .fa-edit:before, 383 | .fa-pencil-square-o:before { 384 | content: "\f044"; 385 | } 386 | .fa-share-square-o:before { 387 | content: "\f045"; 388 | } 389 | .fa-check-square-o:before { 390 | content: "\f046"; 391 | } 392 | .fa-arrows:before { 393 | content: "\f047"; 394 | } 395 | .fa-step-backward:before { 396 | content: "\f048"; 397 | } 398 | .fa-fast-backward:before { 399 | content: "\f049"; 400 | } 401 | .fa-backward:before { 402 | content: "\f04a"; 403 | } 404 | .fa-play:before { 405 | content: "\f04b"; 406 | } 407 | .fa-pause:before { 408 | content: "\f04c"; 409 | } 410 | .fa-stop:before { 411 | content: "\f04d"; 412 | } 413 | .fa-forward:before { 414 | content: "\f04e"; 415 | } 416 | .fa-fast-forward:before { 417 | content: "\f050"; 418 | } 419 | .fa-step-forward:before { 420 | content: "\f051"; 421 | } 422 | .fa-eject:before { 423 | content: "\f052"; 424 | } 425 | .fa-chevron-left:before { 426 | content: "\f053"; 427 | } 428 | .fa-chevron-right:before { 429 | content: "\f054"; 430 | } 431 | .fa-plus-circle:before { 432 | content: "\f055"; 433 | } 434 | .fa-minus-circle:before { 435 | content: "\f056"; 436 | } 437 | .fa-times-circle:before { 438 | content: "\f057"; 439 | } 440 | .fa-check-circle:before { 441 | content: "\f058"; 442 | } 443 | .fa-question-circle:before { 444 | content: "\f059"; 445 | } 446 | .fa-info-circle:before { 447 | content: "\f05a"; 448 | } 449 | .fa-crosshairs:before { 450 | content: "\f05b"; 451 | } 452 | .fa-times-circle-o:before { 453 | content: "\f05c"; 454 | } 455 | .fa-check-circle-o:before { 456 | content: "\f05d"; 457 | } 458 | .fa-ban:before { 459 | content: "\f05e"; 460 | } 461 | .fa-arrow-left:before { 462 | content: "\f060"; 463 | } 464 | .fa-arrow-right:before { 465 | content: "\f061"; 466 | } 467 | .fa-arrow-up:before { 468 | content: "\f062"; 469 | } 470 | .fa-arrow-down:before { 471 | content: "\f063"; 472 | } 473 | .fa-mail-forward:before, 474 | .fa-share:before { 475 | content: "\f064"; 476 | } 477 | .fa-expand:before { 478 | content: "\f065"; 479 | } 480 | .fa-compress:before { 481 | content: "\f066"; 482 | } 483 | .fa-plus:before { 484 | content: "\f067"; 485 | } 486 | .fa-minus:before { 487 | content: "\f068"; 488 | } 489 | .fa-asterisk:before { 490 | content: "\f069"; 491 | } 492 | .fa-exclamation-circle:before { 493 | content: "\f06a"; 494 | } 495 | .fa-gift:before { 496 | content: "\f06b"; 497 | } 498 | .fa-leaf:before { 499 | content: "\f06c"; 500 | } 501 | .fa-fire:before { 502 | content: "\f06d"; 503 | } 504 | .fa-eye:before { 505 | content: "\f06e"; 506 | } 507 | .fa-eye-slash:before { 508 | content: "\f070"; 509 | } 510 | .fa-warning:before, 511 | .fa-exclamation-triangle:before { 512 | content: "\f071"; 513 | } 514 | .fa-plane:before { 515 | content: "\f072"; 516 | } 517 | .fa-calendar:before { 518 | content: "\f073"; 519 | } 520 | .fa-random:before { 521 | content: "\f074"; 522 | } 523 | .fa-comment:before { 524 | content: "\f075"; 525 | } 526 | .fa-magnet:before { 527 | content: "\f076"; 528 | } 529 | .fa-chevron-up:before { 530 | content: "\f077"; 531 | } 532 | .fa-chevron-down:before { 533 | content: "\f078"; 534 | } 535 | .fa-retweet:before { 536 | content: "\f079"; 537 | } 538 | .fa-shopping-cart:before { 539 | content: "\f07a"; 540 | } 541 | .fa-folder:before { 542 | content: "\f07b"; 543 | } 544 | .fa-folder-open:before { 545 | content: "\f07c"; 546 | } 547 | .fa-arrows-v:before { 548 | content: "\f07d"; 549 | } 550 | .fa-arrows-h:before { 551 | content: "\f07e"; 552 | } 553 | .fa-bar-chart-o:before, 554 | .fa-bar-chart:before { 555 | content: "\f080"; 556 | } 557 | .fa-twitter-square:before { 558 | content: "\f081"; 559 | } 560 | .fa-facebook-square:before { 561 | content: "\f082"; 562 | } 563 | .fa-camera-retro:before { 564 | content: "\f083"; 565 | } 566 | .fa-key:before { 567 | content: "\f084"; 568 | } 569 | .fa-gears:before, 570 | .fa-cogs:before { 571 | content: "\f085"; 572 | } 573 | .fa-comments:before { 574 | content: "\f086"; 575 | } 576 | .fa-thumbs-o-up:before { 577 | content: "\f087"; 578 | } 579 | .fa-thumbs-o-down:before { 580 | content: "\f088"; 581 | } 582 | .fa-star-half:before { 583 | content: "\f089"; 584 | } 585 | .fa-heart-o:before { 586 | content: "\f08a"; 587 | } 588 | .fa-sign-out:before { 589 | content: "\f08b"; 590 | } 591 | .fa-linkedin-square:before { 592 | content: "\f08c"; 593 | } 594 | .fa-thumb-tack:before { 595 | content: "\f08d"; 596 | } 597 | .fa-external-link:before { 598 | content: "\f08e"; 599 | } 600 | .fa-sign-in:before { 601 | content: "\f090"; 602 | } 603 | .fa-trophy:before { 604 | content: "\f091"; 605 | } 606 | .fa-github-square:before { 607 | content: "\f092"; 608 | } 609 | .fa-upload:before { 610 | content: "\f093"; 611 | } 612 | .fa-lemon-o:before { 613 | content: "\f094"; 614 | } 615 | .fa-phone:before { 616 | content: "\f095"; 617 | } 618 | .fa-square-o:before { 619 | content: "\f096"; 620 | } 621 | .fa-bookmark-o:before { 622 | content: "\f097"; 623 | } 624 | .fa-phone-square:before { 625 | content: "\f098"; 626 | } 627 | .fa-twitter:before { 628 | content: "\f099"; 629 | } 630 | .fa-facebook-f:before, 631 | .fa-facebook:before { 632 | content: "\f09a"; 633 | } 634 | .fa-github:before { 635 | content: "\f09b"; 636 | } 637 | .fa-unlock:before { 638 | content: "\f09c"; 639 | } 640 | .fa-credit-card:before { 641 | content: "\f09d"; 642 | } 643 | .fa-feed:before, 644 | .fa-rss:before { 645 | content: "\f09e"; 646 | } 647 | .fa-hdd-o:before { 648 | content: "\f0a0"; 649 | } 650 | .fa-bullhorn:before { 651 | content: "\f0a1"; 652 | } 653 | .fa-bell:before { 654 | content: "\f0f3"; 655 | } 656 | .fa-certificate:before { 657 | content: "\f0a3"; 658 | } 659 | .fa-hand-o-right:before { 660 | content: "\f0a4"; 661 | } 662 | .fa-hand-o-left:before { 663 | content: "\f0a5"; 664 | } 665 | .fa-hand-o-up:before { 666 | content: "\f0a6"; 667 | } 668 | .fa-hand-o-down:before { 669 | content: "\f0a7"; 670 | } 671 | .fa-arrow-circle-left:before { 672 | content: "\f0a8"; 673 | } 674 | .fa-arrow-circle-right:before { 675 | content: "\f0a9"; 676 | } 677 | .fa-arrow-circle-up:before { 678 | content: "\f0aa"; 679 | } 680 | .fa-arrow-circle-down:before { 681 | content: "\f0ab"; 682 | } 683 | .fa-globe:before { 684 | content: "\f0ac"; 685 | } 686 | .fa-wrench:before { 687 | content: "\f0ad"; 688 | } 689 | .fa-tasks:before { 690 | content: "\f0ae"; 691 | } 692 | .fa-filter:before { 693 | content: "\f0b0"; 694 | } 695 | .fa-briefcase:before { 696 | content: "\f0b1"; 697 | } 698 | .fa-arrows-alt:before { 699 | content: "\f0b2"; 700 | } 701 | .fa-group:before, 702 | .fa-users:before { 703 | content: "\f0c0"; 704 | } 705 | .fa-chain:before, 706 | .fa-link:before { 707 | content: "\f0c1"; 708 | } 709 | .fa-cloud:before { 710 | content: "\f0c2"; 711 | } 712 | .fa-flask:before { 713 | content: "\f0c3"; 714 | } 715 | .fa-cut:before, 716 | .fa-scissors:before { 717 | content: "\f0c4"; 718 | } 719 | .fa-copy:before, 720 | .fa-files-o:before { 721 | content: "\f0c5"; 722 | } 723 | .fa-paperclip:before { 724 | content: "\f0c6"; 725 | } 726 | .fa-save:before, 727 | .fa-floppy-o:before { 728 | content: "\f0c7"; 729 | } 730 | .fa-square:before { 731 | content: "\f0c8"; 732 | } 733 | .fa-navicon:before, 734 | .fa-reorder:before, 735 | .fa-bars:before { 736 | content: "\f0c9"; 737 | } 738 | .fa-list-ul:before { 739 | content: "\f0ca"; 740 | } 741 | .fa-list-ol:before { 742 | content: "\f0cb"; 743 | } 744 | .fa-strikethrough:before { 745 | content: "\f0cc"; 746 | } 747 | .fa-underline:before { 748 | content: "\f0cd"; 749 | } 750 | .fa-table:before { 751 | content: "\f0ce"; 752 | } 753 | .fa-magic:before { 754 | content: "\f0d0"; 755 | } 756 | .fa-truck:before { 757 | content: "\f0d1"; 758 | } 759 | .fa-pinterest:before { 760 | content: "\f0d2"; 761 | } 762 | .fa-pinterest-square:before { 763 | content: "\f0d3"; 764 | } 765 | .fa-google-plus-square:before { 766 | content: "\f0d4"; 767 | } 768 | .fa-google-plus:before { 769 | content: "\f0d5"; 770 | } 771 | .fa-money:before { 772 | content: "\f0d6"; 773 | } 774 | .fa-caret-down:before { 775 | content: "\f0d7"; 776 | } 777 | .fa-caret-up:before { 778 | content: "\f0d8"; 779 | } 780 | .fa-caret-left:before { 781 | content: "\f0d9"; 782 | } 783 | .fa-caret-right:before { 784 | content: "\f0da"; 785 | } 786 | .fa-columns:before { 787 | content: "\f0db"; 788 | } 789 | .fa-unsorted:before, 790 | .fa-sort:before { 791 | content: "\f0dc"; 792 | } 793 | .fa-sort-down:before, 794 | .fa-sort-desc:before { 795 | content: "\f0dd"; 796 | } 797 | .fa-sort-up:before, 798 | .fa-sort-asc:before { 799 | content: "\f0de"; 800 | } 801 | .fa-envelope:before { 802 | content: "\f0e0"; 803 | } 804 | .fa-linkedin:before { 805 | content: "\f0e1"; 806 | } 807 | .fa-rotate-left:before, 808 | .fa-undo:before { 809 | content: "\f0e2"; 810 | } 811 | .fa-legal:before, 812 | .fa-gavel:before { 813 | content: "\f0e3"; 814 | } 815 | .fa-dashboard:before, 816 | .fa-tachometer:before { 817 | content: "\f0e4"; 818 | } 819 | .fa-comment-o:before { 820 | content: "\f0e5"; 821 | } 822 | .fa-comments-o:before { 823 | content: "\f0e6"; 824 | } 825 | .fa-flash:before, 826 | .fa-bolt:before { 827 | content: "\f0e7"; 828 | } 829 | .fa-sitemap:before { 830 | content: "\f0e8"; 831 | } 832 | .fa-umbrella:before { 833 | content: "\f0e9"; 834 | } 835 | .fa-paste:before, 836 | .fa-clipboard:before { 837 | content: "\f0ea"; 838 | } 839 | .fa-lightbulb-o:before { 840 | content: "\f0eb"; 841 | } 842 | .fa-exchange:before { 843 | content: "\f0ec"; 844 | } 845 | .fa-cloud-download:before { 846 | content: "\f0ed"; 847 | } 848 | .fa-cloud-upload:before { 849 | content: "\f0ee"; 850 | } 851 | .fa-user-md:before { 852 | content: "\f0f0"; 853 | } 854 | .fa-stethoscope:before { 855 | content: "\f0f1"; 856 | } 857 | .fa-suitcase:before { 858 | content: "\f0f2"; 859 | } 860 | .fa-bell-o:before { 861 | content: "\f0a2"; 862 | } 863 | .fa-coffee:before { 864 | content: "\f0f4"; 865 | } 866 | .fa-cutlery:before { 867 | content: "\f0f5"; 868 | } 869 | .fa-file-text-o:before { 870 | content: "\f0f6"; 871 | } 872 | .fa-building-o:before { 873 | content: "\f0f7"; 874 | } 875 | .fa-hospital-o:before { 876 | content: "\f0f8"; 877 | } 878 | .fa-ambulance:before { 879 | content: "\f0f9"; 880 | } 881 | .fa-medkit:before { 882 | content: "\f0fa"; 883 | } 884 | .fa-fighter-jet:before { 885 | content: "\f0fb"; 886 | } 887 | .fa-beer:before { 888 | content: "\f0fc"; 889 | } 890 | .fa-h-square:before { 891 | content: "\f0fd"; 892 | } 893 | .fa-plus-square:before { 894 | content: "\f0fe"; 895 | } 896 | .fa-angle-double-left:before { 897 | content: "\f100"; 898 | } 899 | .fa-angle-double-right:before { 900 | content: "\f101"; 901 | } 902 | .fa-angle-double-up:before { 903 | content: "\f102"; 904 | } 905 | .fa-angle-double-down:before { 906 | content: "\f103"; 907 | } 908 | .fa-angle-left:before { 909 | content: "\f104"; 910 | } 911 | .fa-angle-right:before { 912 | content: "\f105"; 913 | } 914 | .fa-angle-up:before { 915 | content: "\f106"; 916 | } 917 | .fa-angle-down:before { 918 | content: "\f107"; 919 | } 920 | .fa-desktop:before { 921 | content: "\f108"; 922 | } 923 | .fa-laptop:before { 924 | content: "\f109"; 925 | } 926 | .fa-tablet:before { 927 | content: "\f10a"; 928 | } 929 | .fa-mobile-phone:before, 930 | .fa-mobile:before { 931 | content: "\f10b"; 932 | } 933 | .fa-circle-o:before { 934 | content: "\f10c"; 935 | } 936 | .fa-quote-left:before { 937 | content: "\f10d"; 938 | } 939 | .fa-quote-right:before { 940 | content: "\f10e"; 941 | } 942 | .fa-spinner:before { 943 | content: "\f110"; 944 | } 945 | .fa-circle:before { 946 | content: "\f111"; 947 | } 948 | .fa-mail-reply:before, 949 | .fa-reply:before { 950 | content: "\f112"; 951 | } 952 | .fa-github-alt:before { 953 | content: "\f113"; 954 | } 955 | .fa-folder-o:before { 956 | content: "\f114"; 957 | } 958 | .fa-folder-open-o:before { 959 | content: "\f115"; 960 | } 961 | .fa-smile-o:before { 962 | content: "\f118"; 963 | } 964 | .fa-frown-o:before { 965 | content: "\f119"; 966 | } 967 | .fa-meh-o:before { 968 | content: "\f11a"; 969 | } 970 | .fa-gamepad:before { 971 | content: "\f11b"; 972 | } 973 | .fa-keyboard-o:before { 974 | content: "\f11c"; 975 | } 976 | .fa-flag-o:before { 977 | content: "\f11d"; 978 | } 979 | .fa-flag-checkered:before { 980 | content: "\f11e"; 981 | } 982 | .fa-terminal:before { 983 | content: "\f120"; 984 | } 985 | .fa-code:before { 986 | content: "\f121"; 987 | } 988 | .fa-mail-reply-all:before, 989 | .fa-reply-all:before { 990 | content: "\f122"; 991 | } 992 | .fa-star-half-empty:before, 993 | .fa-star-half-full:before, 994 | .fa-star-half-o:before { 995 | content: "\f123"; 996 | } 997 | .fa-location-arrow:before { 998 | content: "\f124"; 999 | } 1000 | .fa-crop:before { 1001 | content: "\f125"; 1002 | } 1003 | .fa-code-fork:before { 1004 | content: "\f126"; 1005 | } 1006 | .fa-unlink:before, 1007 | .fa-chain-broken:before { 1008 | content: "\f127"; 1009 | } 1010 | .fa-question:before { 1011 | content: "\f128"; 1012 | } 1013 | .fa-info:before { 1014 | content: "\f129"; 1015 | } 1016 | .fa-exclamation:before { 1017 | content: "\f12a"; 1018 | } 1019 | .fa-superscript:before { 1020 | content: "\f12b"; 1021 | } 1022 | .fa-subscript:before { 1023 | content: "\f12c"; 1024 | } 1025 | .fa-eraser:before { 1026 | content: "\f12d"; 1027 | } 1028 | .fa-puzzle-piece:before { 1029 | content: "\f12e"; 1030 | } 1031 | .fa-microphone:before { 1032 | content: "\f130"; 1033 | } 1034 | .fa-microphone-slash:before { 1035 | content: "\f131"; 1036 | } 1037 | .fa-shield:before { 1038 | content: "\f132"; 1039 | } 1040 | .fa-calendar-o:before { 1041 | content: "\f133"; 1042 | } 1043 | .fa-fire-extinguisher:before { 1044 | content: "\f134"; 1045 | } 1046 | .fa-rocket:before { 1047 | content: "\f135"; 1048 | } 1049 | .fa-maxcdn:before { 1050 | content: "\f136"; 1051 | } 1052 | .fa-chevron-circle-left:before { 1053 | content: "\f137"; 1054 | } 1055 | .fa-chevron-circle-right:before { 1056 | content: "\f138"; 1057 | } 1058 | .fa-chevron-circle-up:before { 1059 | content: "\f139"; 1060 | } 1061 | .fa-chevron-circle-down:before { 1062 | content: "\f13a"; 1063 | } 1064 | .fa-html5:before { 1065 | content: "\f13b"; 1066 | } 1067 | .fa-css3:before { 1068 | content: "\f13c"; 1069 | } 1070 | .fa-anchor:before { 1071 | content: "\f13d"; 1072 | } 1073 | .fa-unlock-alt:before { 1074 | content: "\f13e"; 1075 | } 1076 | .fa-bullseye:before { 1077 | content: "\f140"; 1078 | } 1079 | .fa-ellipsis-h:before { 1080 | content: "\f141"; 1081 | } 1082 | .fa-ellipsis-v:before { 1083 | content: "\f142"; 1084 | } 1085 | .fa-rss-square:before { 1086 | content: "\f143"; 1087 | } 1088 | .fa-play-circle:before { 1089 | content: "\f144"; 1090 | } 1091 | .fa-ticket:before { 1092 | content: "\f145"; 1093 | } 1094 | .fa-minus-square:before { 1095 | content: "\f146"; 1096 | } 1097 | .fa-minus-square-o:before { 1098 | content: "\f147"; 1099 | } 1100 | .fa-level-up:before { 1101 | content: "\f148"; 1102 | } 1103 | .fa-level-down:before { 1104 | content: "\f149"; 1105 | } 1106 | .fa-check-square:before { 1107 | content: "\f14a"; 1108 | } 1109 | .fa-pencil-square:before { 1110 | content: "\f14b"; 1111 | } 1112 | .fa-external-link-square:before { 1113 | content: "\f14c"; 1114 | } 1115 | .fa-share-square:before { 1116 | content: "\f14d"; 1117 | } 1118 | .fa-compass:before { 1119 | content: "\f14e"; 1120 | } 1121 | .fa-toggle-down:before, 1122 | .fa-caret-square-o-down:before { 1123 | content: "\f150"; 1124 | } 1125 | .fa-toggle-up:before, 1126 | .fa-caret-square-o-up:before { 1127 | content: "\f151"; 1128 | } 1129 | .fa-toggle-right:before, 1130 | .fa-caret-square-o-right:before { 1131 | content: "\f152"; 1132 | } 1133 | .fa-euro:before, 1134 | .fa-eur:before { 1135 | content: "\f153"; 1136 | } 1137 | .fa-gbp:before { 1138 | content: "\f154"; 1139 | } 1140 | .fa-dollar:before, 1141 | .fa-usd:before { 1142 | content: "\f155"; 1143 | } 1144 | .fa-rupee:before, 1145 | .fa-inr:before { 1146 | content: "\f156"; 1147 | } 1148 | .fa-cny:before, 1149 | .fa-rmb:before, 1150 | .fa-yen:before, 1151 | .fa-jpy:before { 1152 | content: "\f157"; 1153 | } 1154 | .fa-ruble:before, 1155 | .fa-rouble:before, 1156 | .fa-rub:before { 1157 | content: "\f158"; 1158 | } 1159 | .fa-won:before, 1160 | .fa-krw:before { 1161 | content: "\f159"; 1162 | } 1163 | .fa-bitcoin:before, 1164 | .fa-btc:before { 1165 | content: "\f15a"; 1166 | } 1167 | .fa-file:before { 1168 | content: "\f15b"; 1169 | } 1170 | .fa-file-text:before { 1171 | content: "\f15c"; 1172 | } 1173 | .fa-sort-alpha-asc:before { 1174 | content: "\f15d"; 1175 | } 1176 | .fa-sort-alpha-desc:before { 1177 | content: "\f15e"; 1178 | } 1179 | .fa-sort-amount-asc:before { 1180 | content: "\f160"; 1181 | } 1182 | .fa-sort-amount-desc:before { 1183 | content: "\f161"; 1184 | } 1185 | .fa-sort-numeric-asc:before { 1186 | content: "\f162"; 1187 | } 1188 | .fa-sort-numeric-desc:before { 1189 | content: "\f163"; 1190 | } 1191 | .fa-thumbs-up:before { 1192 | content: "\f164"; 1193 | } 1194 | .fa-thumbs-down:before { 1195 | content: "\f165"; 1196 | } 1197 | .fa-youtube-square:before { 1198 | content: "\f166"; 1199 | } 1200 | .fa-youtube:before { 1201 | content: "\f167"; 1202 | } 1203 | .fa-xing:before { 1204 | content: "\f168"; 1205 | } 1206 | .fa-xing-square:before { 1207 | content: "\f169"; 1208 | } 1209 | .fa-youtube-play:before { 1210 | content: "\f16a"; 1211 | } 1212 | .fa-dropbox:before { 1213 | content: "\f16b"; 1214 | } 1215 | .fa-stack-overflow:before { 1216 | content: "\f16c"; 1217 | } 1218 | .fa-instagram:before { 1219 | content: "\f16d"; 1220 | } 1221 | .fa-flickr:before { 1222 | content: "\f16e"; 1223 | } 1224 | .fa-adn:before { 1225 | content: "\f170"; 1226 | } 1227 | .fa-bitbucket:before { 1228 | content: "\f171"; 1229 | } 1230 | .fa-bitbucket-square:before { 1231 | content: "\f172"; 1232 | } 1233 | .fa-tumblr:before { 1234 | content: "\f173"; 1235 | } 1236 | .fa-tumblr-square:before { 1237 | content: "\f174"; 1238 | } 1239 | .fa-long-arrow-down:before { 1240 | content: "\f175"; 1241 | } 1242 | .fa-long-arrow-up:before { 1243 | content: "\f176"; 1244 | } 1245 | .fa-long-arrow-left:before { 1246 | content: "\f177"; 1247 | } 1248 | .fa-long-arrow-right:before { 1249 | content: "\f178"; 1250 | } 1251 | .fa-apple:before { 1252 | content: "\f179"; 1253 | } 1254 | .fa-windows:before { 1255 | content: "\f17a"; 1256 | } 1257 | .fa-android:before { 1258 | content: "\f17b"; 1259 | } 1260 | .fa-linux:before { 1261 | content: "\f17c"; 1262 | } 1263 | .fa-dribbble:before { 1264 | content: "\f17d"; 1265 | } 1266 | .fa-skype:before { 1267 | content: "\f17e"; 1268 | } 1269 | .fa-foursquare:before { 1270 | content: "\f180"; 1271 | } 1272 | .fa-trello:before { 1273 | content: "\f181"; 1274 | } 1275 | .fa-female:before { 1276 | content: "\f182"; 1277 | } 1278 | .fa-male:before { 1279 | content: "\f183"; 1280 | } 1281 | .fa-gittip:before, 1282 | .fa-gratipay:before { 1283 | content: "\f184"; 1284 | } 1285 | .fa-sun-o:before { 1286 | content: "\f185"; 1287 | } 1288 | .fa-moon-o:before { 1289 | content: "\f186"; 1290 | } 1291 | .fa-archive:before { 1292 | content: "\f187"; 1293 | } 1294 | .fa-bug:before { 1295 | content: "\f188"; 1296 | } 1297 | .fa-vk:before { 1298 | content: "\f189"; 1299 | } 1300 | .fa-weibo:before { 1301 | content: "\f18a"; 1302 | } 1303 | .fa-renren:before { 1304 | content: "\f18b"; 1305 | } 1306 | .fa-pagelines:before { 1307 | content: "\f18c"; 1308 | } 1309 | .fa-stack-exchange:before { 1310 | content: "\f18d"; 1311 | } 1312 | .fa-arrow-circle-o-right:before { 1313 | content: "\f18e"; 1314 | } 1315 | .fa-arrow-circle-o-left:before { 1316 | content: "\f190"; 1317 | } 1318 | .fa-toggle-left:before, 1319 | .fa-caret-square-o-left:before { 1320 | content: "\f191"; 1321 | } 1322 | .fa-dot-circle-o:before { 1323 | content: "\f192"; 1324 | } 1325 | .fa-wheelchair:before { 1326 | content: "\f193"; 1327 | } 1328 | .fa-vimeo-square:before { 1329 | content: "\f194"; 1330 | } 1331 | .fa-turkish-lira:before, 1332 | .fa-try:before { 1333 | content: "\f195"; 1334 | } 1335 | .fa-plus-square-o:before { 1336 | content: "\f196"; 1337 | } 1338 | .fa-space-shuttle:before { 1339 | content: "\f197"; 1340 | } 1341 | .fa-slack:before { 1342 | content: "\f198"; 1343 | } 1344 | .fa-envelope-square:before { 1345 | content: "\f199"; 1346 | } 1347 | .fa-wordpress:before { 1348 | content: "\f19a"; 1349 | } 1350 | .fa-openid:before { 1351 | content: "\f19b"; 1352 | } 1353 | .fa-institution:before, 1354 | .fa-bank:before, 1355 | .fa-university:before { 1356 | content: "\f19c"; 1357 | } 1358 | .fa-mortar-board:before, 1359 | .fa-graduation-cap:before { 1360 | content: "\f19d"; 1361 | } 1362 | .fa-yahoo:before { 1363 | content: "\f19e"; 1364 | } 1365 | .fa-google:before { 1366 | content: "\f1a0"; 1367 | } 1368 | .fa-reddit:before { 1369 | content: "\f1a1"; 1370 | } 1371 | .fa-reddit-square:before { 1372 | content: "\f1a2"; 1373 | } 1374 | .fa-stumbleupon-circle:before { 1375 | content: "\f1a3"; 1376 | } 1377 | .fa-stumbleupon:before { 1378 | content: "\f1a4"; 1379 | } 1380 | .fa-delicious:before { 1381 | content: "\f1a5"; 1382 | } 1383 | .fa-digg:before { 1384 | content: "\f1a6"; 1385 | } 1386 | .fa-pied-piper-pp:before { 1387 | content: "\f1a7"; 1388 | } 1389 | .fa-pied-piper-alt:before { 1390 | content: "\f1a8"; 1391 | } 1392 | .fa-drupal:before { 1393 | content: "\f1a9"; 1394 | } 1395 | .fa-joomla:before { 1396 | content: "\f1aa"; 1397 | } 1398 | .fa-language:before { 1399 | content: "\f1ab"; 1400 | } 1401 | .fa-fax:before { 1402 | content: "\f1ac"; 1403 | } 1404 | .fa-building:before { 1405 | content: "\f1ad"; 1406 | } 1407 | .fa-child:before { 1408 | content: "\f1ae"; 1409 | } 1410 | .fa-paw:before { 1411 | content: "\f1b0"; 1412 | } 1413 | .fa-spoon:before { 1414 | content: "\f1b1"; 1415 | } 1416 | .fa-cube:before { 1417 | content: "\f1b2"; 1418 | } 1419 | .fa-cubes:before { 1420 | content: "\f1b3"; 1421 | } 1422 | .fa-behance:before { 1423 | content: "\f1b4"; 1424 | } 1425 | .fa-behance-square:before { 1426 | content: "\f1b5"; 1427 | } 1428 | .fa-steam:before { 1429 | content: "\f1b6"; 1430 | } 1431 | .fa-steam-square:before { 1432 | content: "\f1b7"; 1433 | } 1434 | .fa-recycle:before { 1435 | content: "\f1b8"; 1436 | } 1437 | .fa-automobile:before, 1438 | .fa-car:before { 1439 | content: "\f1b9"; 1440 | } 1441 | .fa-cab:before, 1442 | .fa-taxi:before { 1443 | content: "\f1ba"; 1444 | } 1445 | .fa-tree:before { 1446 | content: "\f1bb"; 1447 | } 1448 | .fa-spotify:before { 1449 | content: "\f1bc"; 1450 | } 1451 | .fa-deviantart:before { 1452 | content: "\f1bd"; 1453 | } 1454 | .fa-soundcloud:before { 1455 | content: "\f1be"; 1456 | } 1457 | .fa-database:before { 1458 | content: "\f1c0"; 1459 | } 1460 | .fa-file-pdf-o:before { 1461 | content: "\f1c1"; 1462 | } 1463 | .fa-file-word-o:before { 1464 | content: "\f1c2"; 1465 | } 1466 | .fa-file-excel-o:before { 1467 | content: "\f1c3"; 1468 | } 1469 | .fa-file-powerpoint-o:before { 1470 | content: "\f1c4"; 1471 | } 1472 | .fa-file-photo-o:before, 1473 | .fa-file-picture-o:before, 1474 | .fa-file-image-o:before { 1475 | content: "\f1c5"; 1476 | } 1477 | .fa-file-zip-o:before, 1478 | .fa-file-archive-o:before { 1479 | content: "\f1c6"; 1480 | } 1481 | .fa-file-sound-o:before, 1482 | .fa-file-audio-o:before { 1483 | content: "\f1c7"; 1484 | } 1485 | .fa-file-movie-o:before, 1486 | .fa-file-video-o:before { 1487 | content: "\f1c8"; 1488 | } 1489 | .fa-file-code-o:before { 1490 | content: "\f1c9"; 1491 | } 1492 | .fa-vine:before { 1493 | content: "\f1ca"; 1494 | } 1495 | .fa-codepen:before { 1496 | content: "\f1cb"; 1497 | } 1498 | .fa-jsfiddle:before { 1499 | content: "\f1cc"; 1500 | } 1501 | .fa-life-bouy:before, 1502 | .fa-life-buoy:before, 1503 | .fa-life-saver:before, 1504 | .fa-support:before, 1505 | .fa-life-ring:before { 1506 | content: "\f1cd"; 1507 | } 1508 | .fa-circle-o-notch:before { 1509 | content: "\f1ce"; 1510 | } 1511 | .fa-ra:before, 1512 | .fa-resistance:before, 1513 | .fa-rebel:before { 1514 | content: "\f1d0"; 1515 | } 1516 | .fa-ge:before, 1517 | .fa-empire:before { 1518 | content: "\f1d1"; 1519 | } 1520 | .fa-git-square:before { 1521 | content: "\f1d2"; 1522 | } 1523 | .fa-git:before { 1524 | content: "\f1d3"; 1525 | } 1526 | .fa-y-combinator-square:before, 1527 | .fa-yc-square:before, 1528 | .fa-hacker-news:before { 1529 | content: "\f1d4"; 1530 | } 1531 | .fa-tencent-weibo:before { 1532 | content: "\f1d5"; 1533 | } 1534 | .fa-qq:before { 1535 | content: "\f1d6"; 1536 | } 1537 | .fa-wechat:before, 1538 | .fa-weixin:before { 1539 | content: "\f1d7"; 1540 | } 1541 | .fa-send:before, 1542 | .fa-paper-plane:before { 1543 | content: "\f1d8"; 1544 | } 1545 | .fa-send-o:before, 1546 | .fa-paper-plane-o:before { 1547 | content: "\f1d9"; 1548 | } 1549 | .fa-history:before { 1550 | content: "\f1da"; 1551 | } 1552 | .fa-circle-thin:before { 1553 | content: "\f1db"; 1554 | } 1555 | .fa-header:before { 1556 | content: "\f1dc"; 1557 | } 1558 | .fa-paragraph:before { 1559 | content: "\f1dd"; 1560 | } 1561 | .fa-sliders:before { 1562 | content: "\f1de"; 1563 | } 1564 | .fa-share-alt:before { 1565 | content: "\f1e0"; 1566 | } 1567 | .fa-share-alt-square:before { 1568 | content: "\f1e1"; 1569 | } 1570 | .fa-bomb:before { 1571 | content: "\f1e2"; 1572 | } 1573 | .fa-soccer-ball-o:before, 1574 | .fa-futbol-o:before { 1575 | content: "\f1e3"; 1576 | } 1577 | .fa-tty:before { 1578 | content: "\f1e4"; 1579 | } 1580 | .fa-binoculars:before { 1581 | content: "\f1e5"; 1582 | } 1583 | .fa-plug:before { 1584 | content: "\f1e6"; 1585 | } 1586 | .fa-slideshare:before { 1587 | content: "\f1e7"; 1588 | } 1589 | .fa-twitch:before { 1590 | content: "\f1e8"; 1591 | } 1592 | .fa-yelp:before { 1593 | content: "\f1e9"; 1594 | } 1595 | .fa-newspaper-o:before { 1596 | content: "\f1ea"; 1597 | } 1598 | .fa-wifi:before { 1599 | content: "\f1eb"; 1600 | } 1601 | .fa-calculator:before { 1602 | content: "\f1ec"; 1603 | } 1604 | .fa-paypal:before { 1605 | content: "\f1ed"; 1606 | } 1607 | .fa-google-wallet:before { 1608 | content: "\f1ee"; 1609 | } 1610 | .fa-cc-visa:before { 1611 | content: "\f1f0"; 1612 | } 1613 | .fa-cc-mastercard:before { 1614 | content: "\f1f1"; 1615 | } 1616 | .fa-cc-discover:before { 1617 | content: "\f1f2"; 1618 | } 1619 | .fa-cc-amex:before { 1620 | content: "\f1f3"; 1621 | } 1622 | .fa-cc-paypal:before { 1623 | content: "\f1f4"; 1624 | } 1625 | .fa-cc-stripe:before { 1626 | content: "\f1f5"; 1627 | } 1628 | .fa-bell-slash:before { 1629 | content: "\f1f6"; 1630 | } 1631 | .fa-bell-slash-o:before { 1632 | content: "\f1f7"; 1633 | } 1634 | .fa-trash:before { 1635 | content: "\f1f8"; 1636 | } 1637 | .fa-copyright:before { 1638 | content: "\f1f9"; 1639 | } 1640 | .fa-at:before { 1641 | content: "\f1fa"; 1642 | } 1643 | .fa-eyedropper:before { 1644 | content: "\f1fb"; 1645 | } 1646 | .fa-paint-brush:before { 1647 | content: "\f1fc"; 1648 | } 1649 | .fa-birthday-cake:before { 1650 | content: "\f1fd"; 1651 | } 1652 | .fa-area-chart:before { 1653 | content: "\f1fe"; 1654 | } 1655 | .fa-pie-chart:before { 1656 | content: "\f200"; 1657 | } 1658 | .fa-line-chart:before { 1659 | content: "\f201"; 1660 | } 1661 | .fa-lastfm:before { 1662 | content: "\f202"; 1663 | } 1664 | .fa-lastfm-square:before { 1665 | content: "\f203"; 1666 | } 1667 | .fa-toggle-off:before { 1668 | content: "\f204"; 1669 | } 1670 | .fa-toggle-on:before { 1671 | content: "\f205"; 1672 | } 1673 | .fa-bicycle:before { 1674 | content: "\f206"; 1675 | } 1676 | .fa-bus:before { 1677 | content: "\f207"; 1678 | } 1679 | .fa-ioxhost:before { 1680 | content: "\f208"; 1681 | } 1682 | .fa-angellist:before { 1683 | content: "\f209"; 1684 | } 1685 | .fa-cc:before { 1686 | content: "\f20a"; 1687 | } 1688 | .fa-shekel:before, 1689 | .fa-sheqel:before, 1690 | .fa-ils:before { 1691 | content: "\f20b"; 1692 | } 1693 | .fa-meanpath:before { 1694 | content: "\f20c"; 1695 | } 1696 | .fa-buysellads:before { 1697 | content: "\f20d"; 1698 | } 1699 | .fa-connectdevelop:before { 1700 | content: "\f20e"; 1701 | } 1702 | .fa-dashcube:before { 1703 | content: "\f210"; 1704 | } 1705 | .fa-forumbee:before { 1706 | content: "\f211"; 1707 | } 1708 | .fa-leanpub:before { 1709 | content: "\f212"; 1710 | } 1711 | .fa-sellsy:before { 1712 | content: "\f213"; 1713 | } 1714 | .fa-shirtsinbulk:before { 1715 | content: "\f214"; 1716 | } 1717 | .fa-simplybuilt:before { 1718 | content: "\f215"; 1719 | } 1720 | .fa-skyatlas:before { 1721 | content: "\f216"; 1722 | } 1723 | .fa-cart-plus:before { 1724 | content: "\f217"; 1725 | } 1726 | .fa-cart-arrow-down:before { 1727 | content: "\f218"; 1728 | } 1729 | .fa-diamond:before { 1730 | content: "\f219"; 1731 | } 1732 | .fa-ship:before { 1733 | content: "\f21a"; 1734 | } 1735 | .fa-user-secret:before { 1736 | content: "\f21b"; 1737 | } 1738 | .fa-motorcycle:before { 1739 | content: "\f21c"; 1740 | } 1741 | .fa-street-view:before { 1742 | content: "\f21d"; 1743 | } 1744 | .fa-heartbeat:before { 1745 | content: "\f21e"; 1746 | } 1747 | .fa-venus:before { 1748 | content: "\f221"; 1749 | } 1750 | .fa-mars:before { 1751 | content: "\f222"; 1752 | } 1753 | .fa-mercury:before { 1754 | content: "\f223"; 1755 | } 1756 | .fa-intersex:before, 1757 | .fa-transgender:before { 1758 | content: "\f224"; 1759 | } 1760 | .fa-transgender-alt:before { 1761 | content: "\f225"; 1762 | } 1763 | .fa-venus-double:before { 1764 | content: "\f226"; 1765 | } 1766 | .fa-mars-double:before { 1767 | content: "\f227"; 1768 | } 1769 | .fa-venus-mars:before { 1770 | content: "\f228"; 1771 | } 1772 | .fa-mars-stroke:before { 1773 | content: "\f229"; 1774 | } 1775 | .fa-mars-stroke-v:before { 1776 | content: "\f22a"; 1777 | } 1778 | .fa-mars-stroke-h:before { 1779 | content: "\f22b"; 1780 | } 1781 | .fa-neuter:before { 1782 | content: "\f22c"; 1783 | } 1784 | .fa-genderless:before { 1785 | content: "\f22d"; 1786 | } 1787 | .fa-facebook-official:before { 1788 | content: "\f230"; 1789 | } 1790 | .fa-pinterest-p:before { 1791 | content: "\f231"; 1792 | } 1793 | .fa-whatsapp:before { 1794 | content: "\f232"; 1795 | } 1796 | .fa-server:before { 1797 | content: "\f233"; 1798 | } 1799 | .fa-user-plus:before { 1800 | content: "\f234"; 1801 | } 1802 | .fa-user-times:before { 1803 | content: "\f235"; 1804 | } 1805 | .fa-hotel:before, 1806 | .fa-bed:before { 1807 | content: "\f236"; 1808 | } 1809 | .fa-viacoin:before { 1810 | content: "\f237"; 1811 | } 1812 | .fa-train:before { 1813 | content: "\f238"; 1814 | } 1815 | .fa-subway:before { 1816 | content: "\f239"; 1817 | } 1818 | .fa-medium:before { 1819 | content: "\f23a"; 1820 | } 1821 | .fa-yc:before, 1822 | .fa-y-combinator:before { 1823 | content: "\f23b"; 1824 | } 1825 | .fa-optin-monster:before { 1826 | content: "\f23c"; 1827 | } 1828 | .fa-opencart:before { 1829 | content: "\f23d"; 1830 | } 1831 | .fa-expeditedssl:before { 1832 | content: "\f23e"; 1833 | } 1834 | .fa-battery-4:before, 1835 | .fa-battery:before, 1836 | .fa-battery-full:before { 1837 | content: "\f240"; 1838 | } 1839 | .fa-battery-3:before, 1840 | .fa-battery-three-quarters:before { 1841 | content: "\f241"; 1842 | } 1843 | .fa-battery-2:before, 1844 | .fa-battery-half:before { 1845 | content: "\f242"; 1846 | } 1847 | .fa-battery-1:before, 1848 | .fa-battery-quarter:before { 1849 | content: "\f243"; 1850 | } 1851 | .fa-battery-0:before, 1852 | .fa-battery-empty:before { 1853 | content: "\f244"; 1854 | } 1855 | .fa-mouse-pointer:before { 1856 | content: "\f245"; 1857 | } 1858 | .fa-i-cursor:before { 1859 | content: "\f246"; 1860 | } 1861 | .fa-object-group:before { 1862 | content: "\f247"; 1863 | } 1864 | .fa-object-ungroup:before { 1865 | content: "\f248"; 1866 | } 1867 | .fa-sticky-note:before { 1868 | content: "\f249"; 1869 | } 1870 | .fa-sticky-note-o:before { 1871 | content: "\f24a"; 1872 | } 1873 | .fa-cc-jcb:before { 1874 | content: "\f24b"; 1875 | } 1876 | .fa-cc-diners-club:before { 1877 | content: "\f24c"; 1878 | } 1879 | .fa-clone:before { 1880 | content: "\f24d"; 1881 | } 1882 | .fa-balance-scale:before { 1883 | content: "\f24e"; 1884 | } 1885 | .fa-hourglass-o:before { 1886 | content: "\f250"; 1887 | } 1888 | .fa-hourglass-1:before, 1889 | .fa-hourglass-start:before { 1890 | content: "\f251"; 1891 | } 1892 | .fa-hourglass-2:before, 1893 | .fa-hourglass-half:before { 1894 | content: "\f252"; 1895 | } 1896 | .fa-hourglass-3:before, 1897 | .fa-hourglass-end:before { 1898 | content: "\f253"; 1899 | } 1900 | .fa-hourglass:before { 1901 | content: "\f254"; 1902 | } 1903 | .fa-hand-grab-o:before, 1904 | .fa-hand-rock-o:before { 1905 | content: "\f255"; 1906 | } 1907 | .fa-hand-stop-o:before, 1908 | .fa-hand-paper-o:before { 1909 | content: "\f256"; 1910 | } 1911 | .fa-hand-scissors-o:before { 1912 | content: "\f257"; 1913 | } 1914 | .fa-hand-lizard-o:before { 1915 | content: "\f258"; 1916 | } 1917 | .fa-hand-spock-o:before { 1918 | content: "\f259"; 1919 | } 1920 | .fa-hand-pointer-o:before { 1921 | content: "\f25a"; 1922 | } 1923 | .fa-hand-peace-o:before { 1924 | content: "\f25b"; 1925 | } 1926 | .fa-trademark:before { 1927 | content: "\f25c"; 1928 | } 1929 | .fa-registered:before { 1930 | content: "\f25d"; 1931 | } 1932 | .fa-creative-commons:before { 1933 | content: "\f25e"; 1934 | } 1935 | .fa-gg:before { 1936 | content: "\f260"; 1937 | } 1938 | .fa-gg-circle:before { 1939 | content: "\f261"; 1940 | } 1941 | .fa-tripadvisor:before { 1942 | content: "\f262"; 1943 | } 1944 | .fa-odnoklassniki:before { 1945 | content: "\f263"; 1946 | } 1947 | .fa-odnoklassniki-square:before { 1948 | content: "\f264"; 1949 | } 1950 | .fa-get-pocket:before { 1951 | content: "\f265"; 1952 | } 1953 | .fa-wikipedia-w:before { 1954 | content: "\f266"; 1955 | } 1956 | .fa-safari:before { 1957 | content: "\f267"; 1958 | } 1959 | .fa-chrome:before { 1960 | content: "\f268"; 1961 | } 1962 | .fa-firefox:before { 1963 | content: "\f269"; 1964 | } 1965 | .fa-opera:before { 1966 | content: "\f26a"; 1967 | } 1968 | .fa-internet-explorer:before { 1969 | content: "\f26b"; 1970 | } 1971 | .fa-tv:before, 1972 | .fa-television:before { 1973 | content: "\f26c"; 1974 | } 1975 | .fa-contao:before { 1976 | content: "\f26d"; 1977 | } 1978 | .fa-500px:before { 1979 | content: "\f26e"; 1980 | } 1981 | .fa-amazon:before { 1982 | content: "\f270"; 1983 | } 1984 | .fa-calendar-plus-o:before { 1985 | content: "\f271"; 1986 | } 1987 | .fa-calendar-minus-o:before { 1988 | content: "\f272"; 1989 | } 1990 | .fa-calendar-times-o:before { 1991 | content: "\f273"; 1992 | } 1993 | .fa-calendar-check-o:before { 1994 | content: "\f274"; 1995 | } 1996 | .fa-industry:before { 1997 | content: "\f275"; 1998 | } 1999 | .fa-map-pin:before { 2000 | content: "\f276"; 2001 | } 2002 | .fa-map-signs:before { 2003 | content: "\f277"; 2004 | } 2005 | .fa-map-o:before { 2006 | content: "\f278"; 2007 | } 2008 | .fa-map:before { 2009 | content: "\f279"; 2010 | } 2011 | .fa-commenting:before { 2012 | content: "\f27a"; 2013 | } 2014 | .fa-commenting-o:before { 2015 | content: "\f27b"; 2016 | } 2017 | .fa-houzz:before { 2018 | content: "\f27c"; 2019 | } 2020 | .fa-vimeo:before { 2021 | content: "\f27d"; 2022 | } 2023 | .fa-black-tie:before { 2024 | content: "\f27e"; 2025 | } 2026 | .fa-fonticons:before { 2027 | content: "\f280"; 2028 | } 2029 | .fa-reddit-alien:before { 2030 | content: "\f281"; 2031 | } 2032 | .fa-edge:before { 2033 | content: "\f282"; 2034 | } 2035 | .fa-credit-card-alt:before { 2036 | content: "\f283"; 2037 | } 2038 | .fa-codiepie:before { 2039 | content: "\f284"; 2040 | } 2041 | .fa-modx:before { 2042 | content: "\f285"; 2043 | } 2044 | .fa-fort-awesome:before { 2045 | content: "\f286"; 2046 | } 2047 | .fa-usb:before { 2048 | content: "\f287"; 2049 | } 2050 | .fa-product-hunt:before { 2051 | content: "\f288"; 2052 | } 2053 | .fa-mixcloud:before { 2054 | content: "\f289"; 2055 | } 2056 | .fa-scribd:before { 2057 | content: "\f28a"; 2058 | } 2059 | .fa-pause-circle:before { 2060 | content: "\f28b"; 2061 | } 2062 | .fa-pause-circle-o:before { 2063 | content: "\f28c"; 2064 | } 2065 | .fa-stop-circle:before { 2066 | content: "\f28d"; 2067 | } 2068 | .fa-stop-circle-o:before { 2069 | content: "\f28e"; 2070 | } 2071 | .fa-shopping-bag:before { 2072 | content: "\f290"; 2073 | } 2074 | .fa-shopping-basket:before { 2075 | content: "\f291"; 2076 | } 2077 | .fa-hashtag:before { 2078 | content: "\f292"; 2079 | } 2080 | .fa-bluetooth:before { 2081 | content: "\f293"; 2082 | } 2083 | .fa-bluetooth-b:before { 2084 | content: "\f294"; 2085 | } 2086 | .fa-percent:before { 2087 | content: "\f295"; 2088 | } 2089 | .fa-gitlab:before { 2090 | content: "\f296"; 2091 | } 2092 | .fa-wpbeginner:before { 2093 | content: "\f297"; 2094 | } 2095 | .fa-wpforms:before { 2096 | content: "\f298"; 2097 | } 2098 | .fa-envira:before { 2099 | content: "\f299"; 2100 | } 2101 | .fa-universal-access:before { 2102 | content: "\f29a"; 2103 | } 2104 | .fa-wheelchair-alt:before { 2105 | content: "\f29b"; 2106 | } 2107 | .fa-question-circle-o:before { 2108 | content: "\f29c"; 2109 | } 2110 | .fa-blind:before { 2111 | content: "\f29d"; 2112 | } 2113 | .fa-audio-description:before { 2114 | content: "\f29e"; 2115 | } 2116 | .fa-volume-control-phone:before { 2117 | content: "\f2a0"; 2118 | } 2119 | .fa-braille:before { 2120 | content: "\f2a1"; 2121 | } 2122 | .fa-assistive-listening-systems:before { 2123 | content: "\f2a2"; 2124 | } 2125 | .fa-asl-interpreting:before, 2126 | .fa-american-sign-language-interpreting:before { 2127 | content: "\f2a3"; 2128 | } 2129 | .fa-deafness:before, 2130 | .fa-hard-of-hearing:before, 2131 | .fa-deaf:before { 2132 | content: "\f2a4"; 2133 | } 2134 | .fa-glide:before { 2135 | content: "\f2a5"; 2136 | } 2137 | .fa-glide-g:before { 2138 | content: "\f2a6"; 2139 | } 2140 | .fa-signing:before, 2141 | .fa-sign-language:before { 2142 | content: "\f2a7"; 2143 | } 2144 | .fa-low-vision:before { 2145 | content: "\f2a8"; 2146 | } 2147 | .fa-viadeo:before { 2148 | content: "\f2a9"; 2149 | } 2150 | .fa-viadeo-square:before { 2151 | content: "\f2aa"; 2152 | } 2153 | .fa-snapchat:before { 2154 | content: "\f2ab"; 2155 | } 2156 | .fa-snapchat-ghost:before { 2157 | content: "\f2ac"; 2158 | } 2159 | .fa-snapchat-square:before { 2160 | content: "\f2ad"; 2161 | } 2162 | .fa-pied-piper:before { 2163 | content: "\f2ae"; 2164 | } 2165 | .fa-first-order:before { 2166 | content: "\f2b0"; 2167 | } 2168 | .fa-yoast:before { 2169 | content: "\f2b1"; 2170 | } 2171 | .fa-themeisle:before { 2172 | content: "\f2b2"; 2173 | } 2174 | .fa-google-plus-circle:before, 2175 | .fa-google-plus-official:before { 2176 | content: "\f2b3"; 2177 | } 2178 | .fa-fa:before, 2179 | .fa-font-awesome:before { 2180 | content: "\f2b4"; 2181 | } 2182 | .fa-handshake-o:before { 2183 | content: "\f2b5"; 2184 | } 2185 | .fa-envelope-open:before { 2186 | content: "\f2b6"; 2187 | } 2188 | .fa-envelope-open-o:before { 2189 | content: "\f2b7"; 2190 | } 2191 | .fa-linode:before { 2192 | content: "\f2b8"; 2193 | } 2194 | .fa-address-book:before { 2195 | content: "\f2b9"; 2196 | } 2197 | .fa-address-book-o:before { 2198 | content: "\f2ba"; 2199 | } 2200 | .fa-vcard:before, 2201 | .fa-address-card:before { 2202 | content: "\f2bb"; 2203 | } 2204 | .fa-vcard-o:before, 2205 | .fa-address-card-o:before { 2206 | content: "\f2bc"; 2207 | } 2208 | .fa-user-circle:before { 2209 | content: "\f2bd"; 2210 | } 2211 | .fa-user-circle-o:before { 2212 | content: "\f2be"; 2213 | } 2214 | .fa-user-o:before { 2215 | content: "\f2c0"; 2216 | } 2217 | .fa-id-badge:before { 2218 | content: "\f2c1"; 2219 | } 2220 | .fa-drivers-license:before, 2221 | .fa-id-card:before { 2222 | content: "\f2c2"; 2223 | } 2224 | .fa-drivers-license-o:before, 2225 | .fa-id-card-o:before { 2226 | content: "\f2c3"; 2227 | } 2228 | .fa-quora:before { 2229 | content: "\f2c4"; 2230 | } 2231 | .fa-free-code-camp:before { 2232 | content: "\f2c5"; 2233 | } 2234 | .fa-telegram:before { 2235 | content: "\f2c6"; 2236 | } 2237 | .fa-thermometer-4:before, 2238 | .fa-thermometer:before, 2239 | .fa-thermometer-full:before { 2240 | content: "\f2c7"; 2241 | } 2242 | .fa-thermometer-3:before, 2243 | .fa-thermometer-three-quarters:before { 2244 | content: "\f2c8"; 2245 | } 2246 | .fa-thermometer-2:before, 2247 | .fa-thermometer-half:before { 2248 | content: "\f2c9"; 2249 | } 2250 | .fa-thermometer-1:before, 2251 | .fa-thermometer-quarter:before { 2252 | content: "\f2ca"; 2253 | } 2254 | .fa-thermometer-0:before, 2255 | .fa-thermometer-empty:before { 2256 | content: "\f2cb"; 2257 | } 2258 | .fa-shower:before { 2259 | content: "\f2cc"; 2260 | } 2261 | .fa-bathtub:before, 2262 | .fa-s15:before, 2263 | .fa-bath:before { 2264 | content: "\f2cd"; 2265 | } 2266 | .fa-podcast:before { 2267 | content: "\f2ce"; 2268 | } 2269 | .fa-window-maximize:before { 2270 | content: "\f2d0"; 2271 | } 2272 | .fa-window-minimize:before { 2273 | content: "\f2d1"; 2274 | } 2275 | .fa-window-restore:before { 2276 | content: "\f2d2"; 2277 | } 2278 | .fa-times-rectangle:before, 2279 | .fa-window-close:before { 2280 | content: "\f2d3"; 2281 | } 2282 | .fa-times-rectangle-o:before, 2283 | .fa-window-close-o:before { 2284 | content: "\f2d4"; 2285 | } 2286 | .fa-bandcamp:before { 2287 | content: "\f2d5"; 2288 | } 2289 | .fa-grav:before { 2290 | content: "\f2d6"; 2291 | } 2292 | .fa-etsy:before { 2293 | content: "\f2d7"; 2294 | } 2295 | .fa-imdb:before { 2296 | content: "\f2d8"; 2297 | } 2298 | .fa-ravelry:before { 2299 | content: "\f2d9"; 2300 | } 2301 | .fa-eercast:before { 2302 | content: "\f2da"; 2303 | } 2304 | .fa-microchip:before { 2305 | content: "\f2db"; 2306 | } 2307 | .fa-snowflake-o:before { 2308 | content: "\f2dc"; 2309 | } 2310 | .fa-superpowers:before { 2311 | content: "\f2dd"; 2312 | } 2313 | .fa-wpexplorer:before { 2314 | content: "\f2de"; 2315 | } 2316 | .fa-meetup:before { 2317 | content: "\f2e0"; 2318 | } 2319 | .sr-only { 2320 | position: absolute; 2321 | width: 1px; 2322 | height: 1px; 2323 | padding: 0; 2324 | margin: -1px; 2325 | overflow: hidden; 2326 | clip: rect(0, 0, 0, 0); 2327 | border: 0; 2328 | } 2329 | .sr-only-focusable:active, 2330 | .sr-only-focusable:focus { 2331 | position: static; 2332 | width: auto; 2333 | height: auto; 2334 | margin: 0; 2335 | overflow: visible; 2336 | clip: auto; 2337 | } 2338 | -------------------------------------------------------------------------------- /doc/static/fonts/FontAwesome.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SlavikMIPT/tgcloud/15a44135e9d5510a80065573390e965198016eef/doc/static/fonts/FontAwesome.otf -------------------------------------------------------------------------------- /doc/static/fonts/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SlavikMIPT/tgcloud/15a44135e9d5510a80065573390e965198016eef/doc/static/fonts/fontawesome-webfont.eot -------------------------------------------------------------------------------- /doc/static/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SlavikMIPT/tgcloud/15a44135e9d5510a80065573390e965198016eef/doc/static/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /doc/static/fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SlavikMIPT/tgcloud/15a44135e9d5510a80065573390e965198016eef/doc/static/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /doc/static/fonts/fontawesome-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SlavikMIPT/tgcloud/15a44135e9d5510a80065573390e965198016eef/doc/static/fonts/fontawesome-webfont.woff2 -------------------------------------------------------------------------------- /doc/static/js/bootstrap.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v4.1.1 (https://getbootstrap.com/) 3 | * Copyright 2011-2018 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) 4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 5 | */ 6 | !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports,require("jquery"),require("popper.js")):"function"==typeof define&&define.amd?define(["exports","jquery","popper.js"],e):e(t.bootstrap={},t.jQuery,t.Popper)}(this,function(t,e,c){"use strict";function i(t,e){for(var n=0;nthis._items.length-1||t<0))if(this._isSliding)P(this._element).one(Q.SLID,function(){return e.to(t)});else{if(n===t)return this.pause(),void this.cycle();var i=ndocument.documentElement.clientHeight;!this._isBodyOverflowing&&t&&(this._element.style.paddingLeft=this._scrollbarWidth+"px"),this._isBodyOverflowing&&!t&&(this._element.style.paddingRight=this._scrollbarWidth+"px")},t._resetAdjustments=function(){this._element.style.paddingLeft="",this._element.style.paddingRight=""},t._checkScrollbar=function(){var t=document.body.getBoundingClientRect();this._isBodyOverflowing=t.left+t.right
',trigger:"hover focus",title:"",delay:0,html:!(_e={AUTO:"auto",TOP:"top",RIGHT:"right",BOTTOM:"bottom",LEFT:"left"}),selector:!(de={animation:"boolean",template:"string",title:"(string|element|function)",trigger:"string",delay:"(number|object)",html:"boolean",selector:"(string|boolean)",placement:"(string|function)",offset:"(number|string)",container:"(string|element|boolean)",fallbackPlacement:"(string|array)",boundary:"(string|element)"}),placement:"top",offset:0,container:!1,fallbackPlacement:"flip",boundary:"scrollParent"},pe="out",ve={HIDE:"hide"+he,HIDDEN:"hidden"+he,SHOW:(me="show")+he,SHOWN:"shown"+he,INSERTED:"inserted"+he,CLICK:"click"+he,FOCUSIN:"focusin"+he,FOCUSOUT:"focusout"+he,MOUSEENTER:"mouseenter"+he,MOUSELEAVE:"mouseleave"+he},Ee="fade",ye="show",Te=".tooltip-inner",Ce=".arrow",Ie="hover",Ae="focus",De="click",be="manual",Se=function(){function i(t,e){if("undefined"==typeof c)throw new TypeError("Bootstrap tooltips require Popper.js (https://popper.js.org)");this._isEnabled=!0,this._timeout=0,this._hoverState="",this._activeTrigger={},this._popper=null,this.element=t,this.config=this._getConfig(e),this.tip=null,this._setListeners()}var t=i.prototype;return t.enable=function(){this._isEnabled=!0},t.disable=function(){this._isEnabled=!1},t.toggleEnabled=function(){this._isEnabled=!this._isEnabled},t.toggle=function(t){if(this._isEnabled)if(t){var e=this.constructor.DATA_KEY,n=oe(t.currentTarget).data(e);n||(n=new this.constructor(t.currentTarget,this._getDelegateConfig()),oe(t.currentTarget).data(e,n)),n._activeTrigger.click=!n._activeTrigger.click,n._isWithActiveTrigger()?n._enter(null,n):n._leave(null,n)}else{if(oe(this.getTipElement()).hasClass(ye))return void this._leave(null,this);this._enter(null,this)}},t.dispose=function(){clearTimeout(this._timeout),oe.removeData(this.element,this.constructor.DATA_KEY),oe(this.element).off(this.constructor.EVENT_KEY),oe(this.element).closest(".modal").off("hide.bs.modal"),this.tip&&oe(this.tip).remove(),this._isEnabled=null,this._timeout=null,this._hoverState=null,(this._activeTrigger=null)!==this._popper&&this._popper.destroy(),this._popper=null,this.element=null,this.config=null,this.tip=null},t.show=function(){var e=this;if("none"===oe(this.element).css("display"))throw new Error("Please use show on visible elements");var t=oe.Event(this.constructor.Event.SHOW);if(this.isWithContent()&&this._isEnabled){oe(this.element).trigger(t);var n=oe.contains(this.element.ownerDocument.documentElement,this.element);if(t.isDefaultPrevented()||!n)return;var i=this.getTipElement(),r=Cn.getUID(this.constructor.NAME);i.setAttribute("id",r),this.element.setAttribute("aria-describedby",r),this.setContent(),this.config.animation&&oe(i).addClass(Ee);var s="function"==typeof this.config.placement?this.config.placement.call(this,i,this.element):this.config.placement,o=this._getAttachment(s);this.addAttachmentClass(o);var a=!1===this.config.container?document.body:oe(this.config.container);oe(i).data(this.constructor.DATA_KEY,this),oe.contains(this.element.ownerDocument.documentElement,this.tip)||oe(i).appendTo(a),oe(this.element).trigger(this.constructor.Event.INSERTED),this._popper=new c(this.element,i,{placement:o,modifiers:{offset:{offset:this.config.offset},flip:{behavior:this.config.fallbackPlacement},arrow:{element:Ce},preventOverflow:{boundariesElement:this.config.boundary}},onCreate:function(t){t.originalPlacement!==t.placement&&e._handlePopperPlacementChange(t)},onUpdate:function(t){e._handlePopperPlacementChange(t)}}),oe(i).addClass(ye),"ontouchstart"in document.documentElement&&oe(document.body).children().on("mouseover",null,oe.noop);var l=function(){e.config.animation&&e._fixTransition();var t=e._hoverState;e._hoverState=null,oe(e.element).trigger(e.constructor.Event.SHOWN),t===pe&&e._leave(null,e)};if(oe(this.tip).hasClass(Ee)){var h=Cn.getTransitionDurationFromElement(this.tip);oe(this.tip).one(Cn.TRANSITION_END,l).emulateTransitionEnd(h)}else l()}},t.hide=function(t){var e=this,n=this.getTipElement(),i=oe.Event(this.constructor.Event.HIDE),r=function(){e._hoverState!==me&&n.parentNode&&n.parentNode.removeChild(n),e._cleanTipClass(),e.element.removeAttribute("aria-describedby"),oe(e.element).trigger(e.constructor.Event.HIDDEN),null!==e._popper&&e._popper.destroy(),t&&t()};if(oe(this.element).trigger(i),!i.isDefaultPrevented()){if(oe(n).removeClass(ye),"ontouchstart"in document.documentElement&&oe(document.body).children().off("mouseover",null,oe.noop),this._activeTrigger[De]=!1,this._activeTrigger[Ae]=!1,this._activeTrigger[Ie]=!1,oe(this.tip).hasClass(Ee)){var s=Cn.getTransitionDurationFromElement(n);oe(n).one(Cn.TRANSITION_END,r).emulateTransitionEnd(s)}else r();this._hoverState=""}},t.update=function(){null!==this._popper&&this._popper.scheduleUpdate()},t.isWithContent=function(){return Boolean(this.getTitle())},t.addAttachmentClass=function(t){oe(this.getTipElement()).addClass(ue+"-"+t)},t.getTipElement=function(){return this.tip=this.tip||oe(this.config.template)[0],this.tip},t.setContent=function(){var t=oe(this.getTipElement());this.setElementContent(t.find(Te),this.getTitle()),t.removeClass(Ee+" "+ye)},t.setElementContent=function(t,e){var n=this.config.html;"object"==typeof e&&(e.nodeType||e.jquery)?n?oe(e).parent().is(t)||t.empty().append(e):t.text(oe(e).text()):t[n?"html":"text"](e)},t.getTitle=function(){var t=this.element.getAttribute("data-original-title");return t||(t="function"==typeof this.config.title?this.config.title.call(this.element):this.config.title),t},t._getAttachment=function(t){return _e[t.toUpperCase()]},t._setListeners=function(){var i=this;this.config.trigger.split(" ").forEach(function(t){if("click"===t)oe(i.element).on(i.constructor.Event.CLICK,i.config.selector,function(t){return i.toggle(t)});else if(t!==be){var e=t===Ie?i.constructor.Event.MOUSEENTER:i.constructor.Event.FOCUSIN,n=t===Ie?i.constructor.Event.MOUSELEAVE:i.constructor.Event.FOCUSOUT;oe(i.element).on(e,i.config.selector,function(t){return i._enter(t)}).on(n,i.config.selector,function(t){return i._leave(t)})}oe(i.element).closest(".modal").on("hide.bs.modal",function(){return i.hide()})}),this.config.selector?this.config=h({},this.config,{trigger:"manual",selector:""}):this._fixTitle()},t._fixTitle=function(){var t=typeof this.element.getAttribute("data-original-title");(this.element.getAttribute("title")||"string"!==t)&&(this.element.setAttribute("data-original-title",this.element.getAttribute("title")||""),this.element.setAttribute("title",""))},t._enter=function(t,e){var n=this.constructor.DATA_KEY;(e=e||oe(t.currentTarget).data(n))||(e=new this.constructor(t.currentTarget,this._getDelegateConfig()),oe(t.currentTarget).data(n,e)),t&&(e._activeTrigger["focusin"===t.type?Ae:Ie]=!0),oe(e.getTipElement()).hasClass(ye)||e._hoverState===me?e._hoverState=me:(clearTimeout(e._timeout),e._hoverState=me,e.config.delay&&e.config.delay.show?e._timeout=setTimeout(function(){e._hoverState===me&&e.show()},e.config.delay.show):e.show())},t._leave=function(t,e){var n=this.constructor.DATA_KEY;(e=e||oe(t.currentTarget).data(n))||(e=new this.constructor(t.currentTarget,this._getDelegateConfig()),oe(t.currentTarget).data(n,e)),t&&(e._activeTrigger["focusout"===t.type?Ae:Ie]=!1),e._isWithActiveTrigger()||(clearTimeout(e._timeout),e._hoverState=pe,e.config.delay&&e.config.delay.hide?e._timeout=setTimeout(function(){e._hoverState===pe&&e.hide()},e.config.delay.hide):e.hide())},t._isWithActiveTrigger=function(){for(var t in this._activeTrigger)if(this._activeTrigger[t])return!0;return!1},t._getConfig=function(t){return"number"==typeof(t=h({},this.constructor.Default,oe(this.element).data(),"object"==typeof t&&t?t:{})).delay&&(t.delay={show:t.delay,hide:t.delay}),"number"==typeof t.title&&(t.title=t.title.toString()),"number"==typeof t.content&&(t.content=t.content.toString()),Cn.typeCheckConfig(ae,t,this.constructor.DefaultType),t},t._getDelegateConfig=function(){var t={};if(this.config)for(var e in this.config)this.constructor.Default[e]!==this.config[e]&&(t[e]=this.config[e]);return t},t._cleanTipClass=function(){var t=oe(this.getTipElement()),e=t.attr("class").match(fe);null!==e&&0

'}),He=h({},Nn.DefaultType,{content:"(string|element|function)"}),We="fade",xe=".popover-header",Ue=".popover-body",Ke={HIDE:"hide"+ke,HIDDEN:"hidden"+ke,SHOW:(Me="show")+ke,SHOWN:"shown"+ke,INSERTED:"inserted"+ke,CLICK:"click"+ke,FOCUSIN:"focusin"+ke,FOCUSOUT:"focusout"+ke,MOUSEENTER:"mouseenter"+ke,MOUSELEAVE:"mouseleave"+ke},Fe=function(t){var e,n;function i(){return t.apply(this,arguments)||this}n=t,(e=i).prototype=Object.create(n.prototype),(e.prototype.constructor=e).__proto__=n;var r=i.prototype;return r.isWithContent=function(){return this.getTitle()||this._getContent()},r.addAttachmentClass=function(t){we(this.getTipElement()).addClass(Le+"-"+t)},r.getTipElement=function(){return this.tip=this.tip||we(this.config.template)[0],this.tip},r.setContent=function(){var t=we(this.getTipElement());this.setElementContent(t.find(xe),this.getTitle());var e=this._getContent();"function"==typeof e&&(e=e.call(this.element)),this.setElementContent(t.find(Ue),e),t.removeClass(We+" "+Me)},r._getContent=function(){return this.element.getAttribute("data-content")||this.config.content},r._cleanTipClass=function(){var t=we(this.getTipElement()),e=t.attr("class").match(je);null!==e&&0=this._offsets[r]&&("undefined"==typeof this._offsets[r+1]||t=o.clientWidth&&n>=o.clientHeight}),l=0a[e]&&!t.escapeWithReference&&(n=J(f[o],a[e]-('right'===e?f.width:f.height))),ae({},o,n)}};return l.forEach(function(e){var t=-1===['left','top'].indexOf(e)?'secondary':'primary';f=le({},f,m[t](e))}),e.offsets.popper=f,e},priority:['left','right','top','bottom'],padding:5,boundariesElement:'scrollParent'},keepTogether:{order:400,enabled:!0,fn:function(e){var t=e.offsets,o=t.popper,n=t.reference,i=e.placement.split('-')[0],r=Z,p=-1!==['top','bottom'].indexOf(i),s=p?'right':'bottom',d=p?'left':'top',a=p?'width':'height';return o[s]r(n[s])&&(e.offsets.popper[d]=r(n[s])),e}},arrow:{order:500,enabled:!0,fn:function(e,o){var n;if(!q(e.instance.modifiers,'arrow','keepTogether'))return e;var i=o.element;if('string'==typeof i){if(i=e.instance.popper.querySelector(i),!i)return e;}else if(!e.instance.popper.contains(i))return console.warn('WARNING: `arrow.element` must be child of its popper element!'),e;var r=e.placement.split('-')[0],p=e.offsets,s=p.popper,d=p.reference,a=-1!==['left','right'].indexOf(r),l=a?'height':'width',f=a?'Top':'Left',m=f.toLowerCase(),h=a?'left':'top',c=a?'bottom':'right',u=S(i)[l];d[c]-us[c]&&(e.offsets.popper[m]+=d[m]+u-s[c]),e.offsets.popper=g(e.offsets.popper);var b=d[m]+d[l]/2-u/2,y=t(e.instance.popper),w=parseFloat(y['margin'+f],10),E=parseFloat(y['border'+f+'Width'],10),v=b-e.offsets.popper[m]-w-E;return v=$(J(s[l]-u,v),0),e.arrowElement=i,e.offsets.arrow=(n={},ae(n,m,Q(v)),ae(n,h,''),n),e},element:'[x-arrow]'},flip:{order:600,enabled:!0,fn:function(e,t){if(W(e.instance.modifiers,'inner'))return e;if(e.flipped&&e.placement===e.originalPlacement)return e;var o=v(e.instance.popper,e.instance.reference,t.padding,t.boundariesElement,e.positionFixed),n=e.placement.split('-')[0],i=T(n),r=e.placement.split('-')[1]||'',p=[];switch(t.behavior){case he.FLIP:p=[n,i];break;case he.CLOCKWISE:p=z(n);break;case he.COUNTERCLOCKWISE:p=z(n,!0);break;default:p=t.behavior;}return p.forEach(function(s,d){if(n!==s||p.length===d+1)return e;n=e.placement.split('-')[0],i=T(n);var a=e.offsets.popper,l=e.offsets.reference,f=Z,m='left'===n&&f(a.right)>f(l.left)||'right'===n&&f(a.left)f(l.top)||'bottom'===n&&f(a.top)f(o.right),g=f(a.top)f(o.bottom),b='left'===n&&h||'right'===n&&c||'top'===n&&g||'bottom'===n&&u,y=-1!==['top','bottom'].indexOf(n),w=!!t.flipVariations&&(y&&'start'===r&&h||y&&'end'===r&&c||!y&&'start'===r&&g||!y&&'end'===r&&u);(m||b||w)&&(e.flipped=!0,(m||b)&&(n=p[d+1]),w&&(r=G(r)),e.placement=n+(r?'-'+r:''),e.offsets.popper=le({},e.offsets.popper,C(e.instance.popper,e.offsets.reference,e.placement)),e=P(e.instance.modifiers,e,'flip'))}),e},behavior:'flip',padding:5,boundariesElement:'viewport'},inner:{order:700,enabled:!1,fn:function(e){var t=e.placement,o=t.split('-')[0],n=e.offsets,i=n.popper,r=n.reference,p=-1!==['left','right'].indexOf(o),s=-1===['top','left'].indexOf(o);return i[p?'left':'top']=r[o]-(s?i[p?'width':'height']:0),e.placement=T(t),e.offsets.popper=g(i),e}},hide:{order:800,enabled:!0,fn:function(e){if(!q(e.instance.modifiers,'hide','preventOverflow'))return e;var t=e.offsets.reference,o=D(e.instance.modifiers,function(e){return'preventOverflow'===e.name}).boundaries;if(t.bottomo.right||t.top>o.bottom||t.right 512: 180 | raise ValueError('The part size must be less or equal to 512KB') 181 | 182 | part_size = int(part_size_kb * 1024) 183 | if part_size % 1024 != 0: 184 | raise ValueError( 185 | 'The part size must be evenly divisible by 1024') 186 | 187 | # Set a default file name if None was specified 188 | file_id = helpers.generate_random_long() 189 | if not file_name: 190 | if isinstance(file, str): 191 | file_name = os.path.basename(file) 192 | else: 193 | file_name = str(file_id) 194 | 195 | # Determine whether the file is too big (over 10MB) or not 196 | # Telegram does make a distinction between smaller or larger files 197 | is_large = file_size > 10 * 1024 * 1024 198 | hash_md5 = hashlib.md5() 199 | if not is_large: 200 | # Calculate the MD5 hash before anything else. 201 | # As this needs to be done always for small files, 202 | # might as well do it before anything else and 203 | # check the cache. 204 | if isinstance(file, str): 205 | with open(file, 'rb') as stream: 206 | file = stream.read() 207 | hash_md5.update(file) 208 | if use_cache: 209 | cached = self.session.get_file( 210 | hash_md5.digest(), file_size, cls=use_cache 211 | ) 212 | if cached: 213 | return cached 214 | 215 | part_count = (file_size + part_size - 1) // part_size 216 | __log__.info('Uploading file of %d bytes in %d chunks of %d', 217 | file_size, part_count, part_size) 218 | 219 | with open(file, 'rb') if isinstance(file, str) else BytesIO(file) as stream: 220 | threads_count = 2 + int((self._upload_threads_count - 2) * float(file_size) / (1024 * 1024 * 10)) 221 | threads_count = min(threads_count, self._upload_threads_count) 222 | threads_count = min(part_count, threads_count) 223 | upload_thread = [] 224 | q_request = Queue() 225 | # spawn threads 226 | for i in range(threads_count): 227 | thread_dl = self.ProcessUpload('thread {0}'.format(i), self, q_request) 228 | thread_dl.start() 229 | upload_thread.append(thread_dl) 230 | for part_index in range(0, part_count, threads_count): 231 | # Read the file by in chunks of size part_size 232 | for part_thread_index in range(threads_count): 233 | if part_index + part_thread_index >= part_count: 234 | break 235 | part = stream.read(part_size) 236 | # The SavePartRequest is different depending on whether 237 | # the file is too large or not (over or less than 10MB) 238 | if is_large: 239 | request = SaveBigFilePartRequest(file_id, part_index + part_thread_index, part_count, part) 240 | else: 241 | request = SaveFilePartRequest(file_id, part_index + part_thread_index, part) 242 | q_request.put(request) 243 | # q_request.join() 244 | job_completed = False 245 | while not job_completed: 246 | for th in upload_thread: 247 | if th: 248 | if th.result is True: 249 | job_completed = True 250 | __log__.debug('Uploaded %d/%d', part_index + 1, part_count) 251 | if progress_callback: 252 | progress_callback(stream.tell(), file_size) 253 | elif th.result is False: 254 | raise RuntimeError('Failed to upload file part {}.'.format(part_index)) 255 | q_request.join() 256 | for i in range(threads_count): 257 | q_request.put(None) 258 | for th in upload_thread: 259 | th.join() 260 | if is_large: 261 | return InputFileBig(file_id, part_count, file_name) 262 | else: 263 | return InputSizedFile( 264 | file_id, part_count, file_name, md5=hash_md5, size=file_size 265 | ) 266 | 267 | class ProcessDownload(Thread): 268 | def __init__(self, name, client, q_request=None): 269 | Thread.__init__(self) 270 | self.name = name 271 | self.client = TelegramClient(client._session_name, client.api_id, client.api_hash, update_workers=None, 272 | spawn_read_thread=False) 273 | self.q_request = q_request 274 | self.result = None 275 | 276 | def run(self): 277 | # print('Thread %s started' % self.name) 278 | time.sleep(random.randrange(20, 200, 10) * 0.001) 279 | if not self.client.is_connected(): 280 | self.client.connect() 281 | while True: 282 | request = self.q_request.get() 283 | if request is None: 284 | break 285 | self.result = None 286 | if isinstance(request, CdnDecrypter): 287 | self.result = request.get_file() 288 | else: 289 | self.result = self.client.invoke(request) 290 | if self.result is False: 291 | break 292 | self.q_request.task_done() 293 | self.client.disconnect() 294 | # print('Thread {0} stopped result {1}'.format(self.name, self.result)) 295 | return 296 | 297 | def download_file(self, 298 | input_location, 299 | file=None, 300 | part_size_kb=None, 301 | file_size=None, 302 | progress_callback=None): 303 | 304 | """ 305 | Downloads the given input location to a file. 306 | 307 | Args: 308 | input_location (:tl:`InputFileLocation`): 309 | The file location from which the file will be downloaded. 310 | 311 | file (`str` | `file`): 312 | The output file path, directory, or stream-like object. 313 | If the path exists and is a file, it will be overwritten. 314 | 315 | part_size_kb (`int`, optional): 316 | Chunk size when downloading files. The larger, the less 317 | requests will be made (up to 512KB maximum). 318 | 319 | file_size (`int`, optional): 320 | The file size that is about to be downloaded, if known. 321 | Only used if ``progress_callback`` is specified. 322 | 323 | progress_callback (`callable`, optional): 324 | A callback function accepting two parameters: 325 | ``(downloaded bytes, total)``. Note that the 326 | ``total`` is the provided ``file_size``. 327 | """ 328 | if not part_size_kb: 329 | if not file_size: 330 | part_size_kb = 64 # Reasonable default 331 | else: 332 | part_size_kb = utils.get_appropriated_part_size(file_size) 333 | 334 | part_size = int(part_size_kb * 1024) 335 | # https://core.telegram.org/api/files says: 336 | # > part_size % 1024 = 0 (divisible by 1KB) 337 | # 338 | # But https://core.telegram.org/cdn (more recent) says: 339 | # > limit must be divisible by 4096 bytes 340 | # So we just stick to the 4096 limit. 341 | if part_size % 4096 != 0: 342 | raise ValueError( 343 | 'The part size must be evenly divisible by 4096.') 344 | 345 | in_memory = file is None 346 | if in_memory: 347 | f = io.BytesIO() 348 | elif isinstance(file, str): 349 | # Ensure that we'll be able to download the media 350 | helpers.ensure_parent_dir_exists(file) 351 | f = open(file, 'wb') 352 | else: 353 | f = file 354 | 355 | # The used client will change if FileMigrateError occurs 356 | client = self 357 | cdn_decrypter = None 358 | input_location = utils.get_input_location(input_location) 359 | download_thread = [] 360 | q_request = [] 361 | 362 | __log__.info('Downloading file in chunks of %d bytes', part_size) 363 | threads_count = 2 + int((self._download_threads_count - 2) * float(file_size) / (1024 * 1024 * 10)) 364 | threads_count = min(threads_count, self._download_threads_count) 365 | # threads_count = 1 366 | # threads_count = min(part_count, threads_count) 367 | try: 368 | offset = 0 369 | result = None 370 | try: 371 | request = GetFileRequest(input_location, offset, part_size) 372 | result = client(request) 373 | if isinstance(result, FileCdnRedirect): 374 | __log__.info('File lives in a CDN') 375 | cdn_decrypter, result = CdnDecrypter.prepare_decrypter(client, self._get_cdn_client(result), result) 376 | else: 377 | f.write(result.bytes) 378 | # if callable(getattr(f, 'flush', None)): 379 | # f.flush() 380 | offset += part_size 381 | except FileMigrateError as e: 382 | __log__.info('File lives in another DC') 383 | client = self._get_exported_client(e.new_dc) 384 | 385 | # if cdn_decrypter: 386 | # result = cdn_decrypter.get_file() 387 | 388 | # if not result.bytes: 389 | # return getattr(result, 'type', '') 390 | # f.write(result.bytes) 391 | __log__.debug('Saved %d more bytes', len(result.bytes)) 392 | if progress_callback: 393 | progress_callback(f.tell(), file_size) 394 | 395 | # spawn threads 396 | for i in range(threads_count): 397 | q_request.append(Queue()) 398 | thread_dl = self.ProcessDownload('thread {0}'.format(i), self, q_request[i]) 399 | thread_dl.start() 400 | download_thread.append(thread_dl) 401 | # offset += part_size 402 | while True: 403 | for i in range(threads_count): 404 | if cdn_decrypter: 405 | q_request[i].put(cdn_decrypter) 406 | else: 407 | request = GetFileRequest(input_location, offset, part_size) 408 | q_request[i].put(request) 409 | offset += part_size 410 | for q in q_request: 411 | q.join() 412 | for th in download_thread: 413 | if th.result and th.result.bytes: 414 | f.write(th.result.bytes) 415 | # if callable(getattr(f, 'flush', None)): 416 | # f.flush() 417 | if progress_callback: 418 | progress_callback(f.tell(), file_size) 419 | else: 420 | for i in range(threads_count): 421 | q_request[i].put(None) 422 | for th in download_thread: 423 | th.join() 424 | return getattr(th.result, 'type', '') 425 | finally: 426 | if client != self: 427 | client.disconnect() 428 | 429 | if cdn_decrypter: 430 | try: 431 | cdn_decrypter.client.disconnect() 432 | except: 433 | pass 434 | if isinstance(file, str): 435 | f.close() 436 | -------------------------------------------------------------------------------- /telegram_create_session.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: UTF-8 -*- 3 | 4 | from __future__ import print_function 5 | from __future__ import unicode_literals 6 | 7 | from telethon.telegram_client import TelegramClient 8 | from telegram_client_x import TelegramClientX 9 | from secret import * 10 | 11 | path_home = './' # os.path.abspath('.') 12 | client = TelegramClientX(entity, api_id, api_hash, update_workers=None, spawn_read_thread=True) 13 | # client = TelegramClient(entity, api_id, api_hash, update_workers=None, spawn_read_thread=True) 14 | client.set_upload_threads_count(24) # 24 15 | client.set_download_threads_count(24) # 8 16 | 17 | client.connect() 18 | 19 | if not client.is_user_authorized(): 20 | client.start() 21 | 22 | client.disconnect() -------------------------------------------------------------------------------- /tgcloud-schematics.xml: -------------------------------------------------------------------------------- 1 | 7Vxbd5s4EP41fnQOQlwfm4vbnu2edns5u33aI4Ow2WLkxXIS769fCRBGIAwYTNI0vARGQkLSNzPfjOTM4M3m8W2CtuvfiY+jma75jzN4O9N1HTga+8Mlh0wCNDeXrJLQz2VHwZfwPywq5tJ96OOdVJESEtFwKws9EsfYo5IMJQl5kKsFJJJ73aIVrgm+eCgS0ivzKP8z9Ok6lwPLPRa8w+FqnXfu6FZWsETej1VC9nHe40yHQXplxRsk2sqHulsjnzyURPBuBm8SQmh2t3m8wRGfXjFx4j16EF87g9druonYA2C3afGi4WXQ5WU2uATHtNxdU3sQATsAHjZNDeimj+Z5B/co2osOdCtibV0v2c2Kpn1kgoCwPsrfYv27J6JgvktB8YZVgNr28VgoWvmKI7xK0IZVuInI3hetsm/NGpY7Y+LSB1Tm4GEdUvxlizz+/MAwLU9KsUQg/7gcsGxu2HMYRTckIknaFFwC3w+4fEcT8gOXSoBmQxcXJQJVfAF8tFtjv9TBAm3CiCvPOxzdYxp6iBVEaImj6wJeoumYxLgoJYmPk0pJWrtofhWhHdcO/o2EDTmkvB9TUy6+WE2cUPxYEuVgeIvJBtPkwKrkpRDkrxyELcjh+nBUIujkOrQu6Q8QFVGuuqui7SP82E2OQDUaTbu2tNhnip0/koSuyYrEKLo7SivT45FN6OX3JQjgx5D+xWeNGYbs6bsoidlHlor44/d8fiXg7ChK6BtunJjA46vAO8rEizAqeor9eiUmLFX5B1N6yDGI9pTwlSyG9oGQbdFjBYJaeikh2Ii6Mt4FPFVNKrHEZ79iZ2vg2pF94uEGc2Lkxh8lKyxes9UITHCEaHgv9zcETXbdWI6OJr0MJ02GE4BuGVDgStMLwH3CScgGhJNfGmnGZZGmdm5TIE+f1o5VgKdLhqzmAV/h9ZPDq+njFKStK0fTdRVHE63stiiuypZlIsfWPJ7vDrFXlCanCVwhKze930YEMSK4YFiNs9um1oqmbj/e/Hb3uaUTFaEstzkOnQROnU5i4JvYVqmAa9kQWTW9H4XDuRKHM7Q6hyuaLXM4xxmOTWsCp/tK4S5J4RSMDU7lN80n9ZuO4VQI2ytfG82hPif/2Z7jENmJ+X7rI4rV/kWr+5ItiaIwXvHB4+Q+9HC5Sgc3DE654R5Ob1h3I6VXQN0fej5eOksVgqEFXeg3+MOa81PgrdEfmrbW7g9F+qLsD90RUhrDqRk8Sc2KlfeiEKcNshnHS55CxUkbQlqI0uVokY+wE3gqGFieg5fBJWAAgQwDoClwADUFL7LHSG25F3dtZcd2JEKZa7NfQ8KhxAh0cWHGRC4MdPBhcvTWwRuYKkPz+e72/RdW+se3u293JZ9Xid2azcqwELTm6i62OaC1m63FQsKoCmqyyiqBN8yZ6bIVM6HCmVkKZzaGETOmzqhWojuYbncd4ztG0AvBS2fo9qXtGzTr9m0qc2Y2WjM/vFcaD6ZBdI6icBVn1iNJkd5Ok3pbJVu5ebngJvEM21TI0nE1WKw+TIrvTDtOD0JdiebSbcdPZBfSkMQc9wwiXIeuuZViKIw+VCosCaVkwyrkk3+bzfzxhTe5nJLU0pIk/I8NARX2lvcfr76mmqLJVkBlqk/vmjZpxCh8Ud4KZdNZs7QGdOqWVnf1ERx880a8Mpt5PpbH0QhBE27b8q4DotUOnz/mkCqteAXMjhUZ4CzL8+rvbvkxkaaJ0FqmaNBQUhKXqYAWhTGeC2jyUuYeuK1VjBRtUmVLn9LmM0GXTPq5s6ye0SBIVbj2jTys/Ztph3l9dTUzb3uEsV1W0WWX9G3to3FVmGF6jnxEUYfNh6GwbNavHuJhHzJSnshWuDXHw54yQbB0TMOsM23Jz6RUPgq370qdKQlW1WcVzm0Mpu7K2zCWOI9Q8h/AUDB13RiBqgO7NfEkTu2JdQXNWOscQipjuOsDxe8/tgSOT7pNFyDfRmq4GZapGTW4pe3ln9K0QdeLbJi6DBbDqIHFNepYEaR9EPl2Lh/VVXddXlp0dvnsk1OPzibblxOdj37A4POnm2eh/723JcbWf8MyWvUfaNZlDAC8eFpH3Kty0zMpo2O+IOPQHJj21n5Nof2TbZfqCu2v4IUH59tGPciP+aOlqK719o+WTKZs3RFH/MsaYivoFDCN4XPgDjeAyux6LT/FQrb94/wHTmL+m4xneqppiTzHhypV0aFhmGfu4mY4q0MgX3K3nuhWrrc1wnKLpPplId804LYRjrEvDVQjHJorVfr4kCcUA+ThFuhOtYkTBIGuDi0bNnG6gVc7by0huAK6qR2vEdCrt9O1HrFft9QiVC19sN/h+fbAT4c+jS3rBod+CfRBcDjIjKsNHeLUwCA0wDPY3ZAzmHrzIUytBw3rTa06p5CKN1YJ8vmpmfKBXWQtLauBd35dh/F53FOGUJ1HnkkLbQUtbEDe6LRQ/GDyiU762i/2qO+IYYMqaTAVPmDz0bfCA9yTCIzq/y8R63cy+G3hiyWFL46jK8IXR8UPxojvm7fXyyuh/xIrUcnKT7sSxgSp1lMHaF6szZzulIxVN6lTnZIRfb/qMZtz/Un1+Gl/qG5pVlWRX8/AjaLdCkI9mXY379j+etptyadcJ9bu5mxvMeM49pLDlp5Ka5zIX/Q74ed56jj2xN6USkNaMDH+WWUI5U0taChSuLpqEUdIcNqtByAajgJpmupAVw0AGzbnJ5e/f1arDyoW7MrzFN1+QfN8UOHaEipcQ7GRczFUdFDt7Cd/uwlUG6fXz6jaFjAnU232ePx3WmlZ6d+Wwbv/AQ== -------------------------------------------------------------------------------- /tgcloud.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Open-source Telegram VFS 3 | [Service] 4 | WorkingDirectory=/home//tgcloud 5 | #ExecStart=/bin/python2.7 dedupfs/dedupfs.py -f --block-size 20971520 -o auto_unmount -o direct_io -o no_splice_write -o no_splice_read -o no_splice_move storage/ 6 | ExecStart=/bin/python2.7 dedupfs/dedupfs.py -f --block-size 20971520 -o hard_remove -o auto_unmount storage/ 7 | Restart=always 8 | RestartSec=10 9 | SyslogIdentifier=tgcloud-dedupfs-fuse 10 | User= 11 | [Install] 12 | WantedBy=multi-user.target --------------------------------------------------------------------------------