├── .gitignore ├── LICENSE ├── README.md ├── Vagrantfile ├── config.example.json ├── project.clj ├── resources ├── libs │ ├── bootstrap │ │ └── bootstrap.js │ ├── jquery │ │ └── jquery-1.10.2.min.js │ └── react │ │ ├── react.externs.js │ │ └── react.min.js ├── public │ ├── css │ │ ├── bootstrap-theme.min.css │ │ ├── bootstrap.min.css │ │ └── style.css │ └── fonts │ │ ├── glyphicons-halflings-regular.eot │ │ ├── glyphicons-halflings-regular.svg │ │ ├── glyphicons-halflings-regular.ttf │ │ └── glyphicons-halflings-regular.woff └── templates │ ├── index.debug.mustache │ ├── index.mustache │ └── tags.mustache ├── src-cljs └── dakait │ ├── components.cljs │ ├── downloads.cljs │ ├── net.cljs │ ├── start.cljs │ └── util.cljs ├── src └── dakait │ ├── assocs.clj │ ├── config.clj │ ├── downloader.clj │ ├── files.clj │ ├── handler.clj │ ├── main.clj │ ├── mdns.clj │ ├── pusher.clj │ ├── staging.clj │ ├── tags.clj │ ├── util.clj │ └── views.clj └── test └── dakait └── test ├── downloader.clj ├── tags.clj └── util.clj /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /lib 3 | /classes 4 | /checkouts 5 | pom.xml 6 | pom.xml.asc 7 | *.jar 8 | *.class 9 | /.lein-* 10 | /.nrepl-port 11 | /resources/public/js/index.js 12 | /resources/public/js/tags.js 13 | /resources/public/js/main.js 14 | .DS_Store 15 | .vagrant 16 | /config.json 17 | /tags.json 18 | /assocs.json 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Eclipse Public License - v 1.0 2 | 3 | THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE 4 | PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF 5 | THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. 6 | 7 | 1. DEFINITIONS 8 | 9 | "Contribution" means: 10 | 11 | a) in the case of the initial Contributor, the initial code and 12 | documentation distributed under this Agreement, and 13 | 14 | b) in the case of each subsequent Contributor: 15 | 16 | i) changes to the Program, and 17 | 18 | ii) additions to the Program; 19 | 20 | where such changes and/or additions to the Program originate from and 21 | are distributed by that particular Contributor. A Contribution 22 | 'originates' from a Contributor if it was added to the Program by such 23 | Contributor itself or anyone acting on such Contributor's 24 | behalf. Contributions do not include additions to the Program which: 25 | (i) are separate modules of software distributed in conjunction with 26 | the Program under their own license agreement, and (ii) are not 27 | derivative works of the Program. 28 | 29 | "Contributor" means any person or entity that distributes the Program. 30 | 31 | "Licensed Patents" mean patent claims licensable by a Contributor 32 | which are necessarily infringed by the use or sale of its Contribution 33 | alone or when combined with the Program. 34 | 35 | "Program" means the Contributions distributed in accordance with this 36 | Agreement. 37 | 38 | "Recipient" means anyone who receives the Program under this 39 | Agreement, including all Contributors. 40 | 41 | 2. GRANT OF RIGHTS 42 | 43 | a) Subject to the terms of this Agreement, each Contributor hereby 44 | grants Recipient a non-exclusive, worldwide, royalty-free copyright 45 | license to reproduce, prepare derivative works of, publicly display, 46 | publicly perform, distribute and sublicense the Contribution of such 47 | Contributor, if any, and such derivative works, in source code and 48 | object code form. 49 | 50 | b) Subject to the terms of this Agreement, each Contributor hereby 51 | grants Recipient a non-exclusive, worldwide, royalty-free patent 52 | license under Licensed Patents to make, use, sell, offer to sell, 53 | import and otherwise transfer the Contribution of such Contributor, if 54 | any, in source code and object code form. This patent license shall 55 | apply to the combination of the Contribution and the Program if, at 56 | the time the Contribution is added by the Contributor, such addition 57 | of the Contribution causes such combination to be covered by the 58 | Licensed Patents. The patent license shall not apply to any other 59 | combinations which include the Contribution. No hardware per se is 60 | licensed hereunder. 61 | 62 | c) Recipient understands that although each Contributor grants the 63 | licenses to its Contributions set forth herein, no assurances are 64 | provided by any Contributor that the Program does not infringe the 65 | patent or other intellectual property rights of any other entity. Each 66 | Contributor disclaims any liability to Recipient for claims brought by 67 | any other entity based on infringement of intellectual property rights 68 | or otherwise. As a condition to exercising the rights and licenses 69 | granted hereunder, each Recipient hereby assumes sole responsibility 70 | to secure any other intellectual property rights needed, if any. For 71 | example, if a third party patent license is required to allow 72 | Recipient to distribute the Program, it is Recipient's responsibility 73 | to acquire that license before distributing the Program. 74 | 75 | d) Each Contributor represents that to its knowledge it has sufficient 76 | copyright rights in its Contribution, if any, to grant the copyright 77 | license set forth in this Agreement. 78 | 79 | 3. REQUIREMENTS 80 | 81 | A Contributor may choose to distribute the Program in object code form 82 | under its own license agreement, provided that: 83 | 84 | a) it complies with the terms and conditions of this Agreement; and 85 | 86 | b) its license agreement: 87 | 88 | i) effectively disclaims on behalf of all Contributors all warranties 89 | and conditions, express and implied, including warranties or 90 | conditions of title and non-infringement, and implied warranties or 91 | conditions of merchantability and fitness for a particular purpose; 92 | 93 | ii) effectively excludes on behalf of all Contributors all liability 94 | for damages, including direct, indirect, special, incidental and 95 | consequential damages, such as lost profits; 96 | 97 | iii) states that any provisions which differ from this Agreement are 98 | offered by that Contributor alone and not by any other party; and 99 | 100 | iv) states that source code for the Program is available from such 101 | Contributor, and informs licensees how to obtain it in a reasonable 102 | manner on or through a medium customarily used for software exchange. 103 | 104 | When the Program is made available in source code form: 105 | 106 | a) it must be made available under this Agreement; and 107 | 108 | b) a copy of this Agreement must be included with each copy of the Program. 109 | 110 | Contributors may not remove or alter any copyright notices contained 111 | within the Program. 112 | 113 | Each Contributor must identify itself as the originator of its 114 | Contribution, if any, in a manner that reasonably allows subsequent 115 | Recipients to identify the originator of the Contribution. 116 | 117 | 4. COMMERCIAL DISTRIBUTION 118 | 119 | Commercial distributors of software may accept certain 120 | responsibilities with respect to end users, business partners and the 121 | like. While this license is intended to facilitate the commercial use 122 | of the Program, the Contributor who includes the Program in a 123 | commercial product offering should do so in a manner which does not 124 | create potential liability for other Contributors. Therefore, if a 125 | Contributor includes the Program in a commercial product offering, 126 | such Contributor ("Commercial Contributor") hereby agrees to defend 127 | and indemnify every other Contributor ("Indemnified Contributor") 128 | against any losses, damages and costs (collectively "Losses") arising 129 | from claims, lawsuits and other legal actions brought by a third party 130 | against the Indemnified Contributor to the extent caused by the acts 131 | or omissions of such Commercial Contributor in connection with its 132 | distribution of the Program in a commercial product offering. The 133 | obligations in this section do not apply to any claims or Losses 134 | relating to any actual or alleged intellectual property 135 | infringement. In order to qualify, an Indemnified Contributor must: a) 136 | promptly notify the Commercial Contributor in writing of such claim, 137 | and b) allow the Commercial Contributor tocontrol, and cooperate with 138 | the Commercial Contributor in, the defense and any related settlement 139 | negotiations. The Indemnified Contributor may participate in any such 140 | claim at its own expense. 141 | 142 | For example, a Contributor might include the Program in a commercial 143 | product offering, Product X. That Contributor is then a Commercial 144 | Contributor. If that Commercial Contributor then makes performance 145 | claims, or offers warranties related to Product X, those performance 146 | claims and warranties are such Commercial Contributor's responsibility 147 | alone. Under this section, the Commercial Contributor would have to 148 | defend claims against the other Contributors related to those 149 | performance claims and warranties, and if a court requires any other 150 | Contributor to pay any damages as a result, the Commercial Contributor 151 | must pay those damages. 152 | 153 | 5. NO WARRANTY 154 | 155 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS 156 | PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 157 | KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY 158 | WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY 159 | OR FITNESS FOR A PARTICULAR PURPOSE. Each Recipient is solely 160 | responsible for determining the appropriateness of using and 161 | distributing the Program and assumes all risks associated with its 162 | exercise of rights under this Agreement , including but not limited to 163 | the risks and costs of program errors, compliance with applicable 164 | laws, damage to or loss of data, programs or equipment, and 165 | unavailability or interruption of operations. 166 | 167 | 6. DISCLAIMER OF LIABILITY 168 | 169 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR 170 | ANY CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, 171 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING 172 | WITHOUT LIMITATION LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF 173 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 174 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OR 175 | DISTRIBUTION OF THE PROGRAM OR THE EXERCISE OF ANY RIGHTS GRANTED 176 | HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 177 | 178 | 7. GENERAL 179 | 180 | If any provision of this Agreement is invalid or unenforceable under 181 | applicable law, it shall not affect the validity or enforceability of 182 | the remainder of the terms of this Agreement, and without further 183 | action by the parties hereto, such provision shall be reformed to the 184 | minimum extent necessary to make such provision valid and enforceable. 185 | 186 | If Recipient institutes patent litigation against any entity 187 | (including a cross-claim or counterclaim in a lawsuit) alleging that 188 | the Program itself (excluding combinations of the Program with other 189 | software or hardware) infringes such Recipient's patent(s), then such 190 | Recipient's rights granted under Section 2(b) shall terminate as of 191 | the date such litigation is filed. 192 | 193 | All Recipient's rights under this Agreement shall terminate if it 194 | fails to comply with any of the material terms or conditions of this 195 | Agreement and does not cure such failure in a reasonable period of 196 | time after becoming aware of such noncompliance. If all Recipient's 197 | rights under this Agreement terminate, Recipient agrees to cease use 198 | and distribution of the Program as soon as reasonably 199 | practicable. However, Recipient's obligations under this Agreement and 200 | any licenses granted by Recipient relating to the Program shall 201 | continue and survive. 202 | 203 | Everyone is permitted to copy and distribute copies of this Agreement, 204 | but in order to avoid inconsistency the Agreement is copyrighted and 205 | may only be modified in the following manner. The Agreement Steward 206 | reserves the right to publish new versions (including revisions) of 207 | this Agreement from time to time. No one other than the Agreement 208 | Steward has the right to modify this Agreement. The Eclipse Foundation 209 | is the initial Agreement Steward. The Eclipse Foundation may assign 210 | the responsibility to serve as the Agreement Steward to a suitable 211 | separate entity. Each new version of the Agreement will be given a 212 | distinguishing version number. The Program (including Contributions) 213 | may always be distributed subject to the version of the Agreement 214 | under which it was received. In addition, after a new version of the 215 | Agreement is published, Contributor may elect to distribute the 216 | Program (including its Contributions) under the new version. Except as 217 | expressly stated in Sections 2(a) and 2(b) above, Recipient receives 218 | no rights or licenses to the intellectual property of any Contributor 219 | under this Agreement, whether expressly, by implication, estoppel or 220 | otherwise. All rights in the Program not expressly granted under this 221 | Agreement are reserved. 222 | 223 | This Agreement is governed by the laws of the State of Washington and 224 | the intellectual property laws of the United States of America. No 225 | party to this Agreement will bring a legal action under this Agreement 226 | more than one year after the cause of action arose. Each party waives 227 | its rights to a jury trial in any resulting litigation. 228 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Dakait 2 | ========= 3 | 4 | Screenshots and more information is available here: http://udayv.com/dakait/ 5 | 6 | Download files from remote servers and effortlessly manage them. 7 | 8 | You have large amounts of large files on a remote server (e.g. a seedbox) and you want to download and stage them nicely. Dakait lets you do that. 9 | 10 | You create tags (pretty looking names for local directories) and tag your remote file on a web-interface. Dakait takes care of downloading and staging them for you. 11 | 12 | Current Release 13 | --- 14 | Current release version is `0.1.3`. It will do what its supposed to do, but it has a long way to go. 15 | 16 | [Download Now](https://github.com/verma/dakait/releases/download/0.1.3/dakait-0.1.3-standalone.jar) 17 | 18 | How to run 19 | --- 20 | 21 | You need a configuration file to run Dakait. Modify and rename `config.example.json` to `config.json`. This file must reside in the working directory (path where you run this program from). 22 | 23 | Once you have the jar file, make sure your server has a recent enough version of java. You can start the server by running the following command 24 | 25 | java -jar dakait-0.1.3-standalone.jar 26 | 27 | This will start a server and bind to port `3000`. Now point your browser to `http://server-address:3000/` 28 | 29 | What are tags? 30 | --- 31 | 32 | Tags are an easy way of classifying a file. 33 | 34 | Lets say you create a tag called `Ubuntu Image` and assign it a target `images/ubuntu`. While browsing the remote server through Dakait's web-interface, whenever you tag a file or a directory with `Ubuntu Image` Dakait will start downlaoding the tagged file/directory to your `/images/ubuntu` Directory. Note that reapplying a tag just initiates a new download, this will change in a future release when Dakait becomes smart enough to detect this. 35 | 36 | Why? 37 | --- 38 | I really wanted to learn clojure and really really needed a tool like this. 39 | 40 | 41 | Configuration Options 42 | -- 43 | Dakait understands the following options 44 | 45 | ### config-data-dir (required) 46 | Local directory to cache running state. Tags and tag associations are saved in this directory. 47 | 48 | ### sftp-host (required) 49 | The remote SFTP host. At this time Dakait support `sftp` protocol only. 50 | 51 | ### private-key (required) 52 | The private key to use to login to the remote server. 53 | 54 | ### local-base-path (required) 55 | Directory to save downloaded files to. The tag target directories will be created under this directory. 56 | 57 | ### sftp-port (optional) 58 | SSH port on remote server 59 | 60 | ### username (optional) 61 | SSH username to use, defaults to the username of the user running the program. 62 | 63 | ### base-path (optional) 64 | The base path to `cd` into on the remote machine, your session starts in this directory, and you cannot traverse back. 65 | 66 | ### concurrency (optional) 67 | Number of simultaneous downloads. Defaults to `4`. 68 | 69 | Contributors 70 | --- 71 | 72 | - Uday Verma (https://github.com/verma) 73 | - Marcin Urbański (https://github.com/murbanski) 74 | 75 | License 76 | --- 77 | 78 | (C) Copyright 2014 Uday Verma 79 | 80 | This software is licensed under Eclipse Public License 1.0. 81 | -------------------------------------------------------------------------------- /Vagrantfile: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | # vi: set ft=ruby : 3 | 4 | # Vagrantfile API/syntax version. Don't touch unless you know what you're doing! 5 | VAGRANTFILE_API_VERSION = "2" 6 | 7 | Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| 8 | config.vm.box = "precise64" 9 | 10 | config.vm.hostname = "dakait-dev" 11 | config.vm.box_url = "http://files.vagrantup.com/precise64.box" 12 | 13 | config.vm.network :forwarded_port, guest: 3000, host: 3000 14 | 15 | config.vm.provider :virtualbox do |vb| 16 | vb.customize ["modifyvm", :id, "--memory", "2048"] 17 | vb.customize ["modifyvm", :id, "--cpus", "2"] 18 | vb.customize ["modifyvm", :id, "--ioapic", "on"] 19 | end 20 | 21 | # 22 | ppaRepos = [ 23 | ] 24 | 25 | # The postgres/gis family of products is not in the list intentionally since they 26 | # are explicitly installed in one of the scripts 27 | packageList = [ 28 | "git", 29 | "build-essential", 30 | "openjdk-7-jdk" 31 | ]; 32 | 33 | if Dir.glob("#{File.dirname(__FILE__)}/.vagrant/machines/default/*/id").empty? 34 | pkg_cmd = "" 35 | 36 | pkg_cmd << "apt-get update -qq; apt-get install -q -y python-software-properties; " 37 | 38 | if ppaRepos.length > 0 39 | ppaRepos.each { |repo| pkg_cmd << "add-apt-repository -y " << repo << " ; " } 40 | pkg_cmd << "apt-get update -qq; " 41 | end 42 | 43 | # install packages we need 44 | pkg_cmd << "apt-get install -q -y " + packageList.join(" ") << " ; " 45 | 46 | # get the latest version of leiningen and setup it up 47 | pkg_cmd << "wget -O /usr/bin/lein https://raw.github.com/technomancy/leiningen/stable/bin/lein ; " 48 | pkg_cmd << "chmod +x /usr/bin/lein ; " 49 | 50 | config.vm.provision :shell, :inline => pkg_cmd 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /config.example.json: -------------------------------------------------------------------------------- 1 | { 2 | "config-data-dir": "/change/this/to/some/dir", 3 | "sftp-host": "sftp.example.com", 4 | "sftp-port": 22, 5 | "use-ipv6": false, 6 | "username": "guest", 7 | "private-key": "/path/to/private/key", 8 | "base-path": "./some/dir", 9 | "local-base-path": "/some/local/dir", 10 | "staging-dir": "/path/to/temporarily/download/files", 11 | "push-path": "/path/to/push/urls/to", 12 | "concurrency": 4 13 | } 14 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject dakait "0.1.3" 2 | :description "A tool to download files from your FTP/SFTP servers in an organized way." 3 | :license {:name "Eclipse Public License" 4 | :url "http://www.eclipse.org/legal/epl-v10.html"} 5 | :url "https://github.com/verma/dakait" 6 | :dependencies [[org.clojure/clojure "1.6.0"] 7 | [org.clojure/core.async "0.1.267.0-0d7780-alpha"] 8 | [org.clojure/core.match "0.2.1"] 9 | [org.clojure/tools.cli "0.3.1"] 10 | [compojure "1.1.8"] 11 | [clj-ssh "0.5.9"] 12 | [ring/ring-devel "1.2.2"] 13 | [ring/ring-core "1.2.2"] 14 | [http-kit "2.1.18"] 15 | [org.clojure/clojurescript "0.0-2202"] 16 | [org.clojure/data.json "0.2.4"] 17 | [de.ubercode.clostache/clostache "1.4.0"] 18 | [org.clojure/tools.logging "0.2.6"] 19 | [javax.jmdns/jmdns "3.4.1"] 20 | [me.raynes/conch "0.5.1"] 21 | [om "0.6.2"] 22 | [jayq "2.5.1"]] 23 | :plugins [[lein-cljsbuild "1.0.3"] 24 | [lein-ancient "0.5.5"]] 25 | 26 | :main dakait.main 27 | 28 | :profiles 29 | {:dev {:dependencies [[javax.servlet/servlet-api "2.5"] 30 | [ring-mock "0.1.5"]]} 31 | :uberjar {:main dakait.main, :aot :all 32 | :dependencies [[javax.servlet/servlet-api "2.5"]]}} 33 | :hooks [leiningen.cljsbuild] 34 | 35 | :cljsbuild { 36 | :builds [ 37 | {:id "rel" 38 | :source-paths ["src-cljs/dakait"] 39 | :compiler {:output-to "resources/public/js/index.js" 40 | :optimizations :advanced 41 | :preamble ["libs/react/react.min.js" 42 | "libs/jquery/jquery-1.10.2.min.js" 43 | "libs/bootstrap/bootstrap.js"] 44 | :externs ["libs/react/react.externs.js" 45 | "libs/jquery/jquery-1.10.2.min.js" 46 | "libs/bootstrap/bootstrap.js"] 47 | :closure-warnings {:externs-validation :off 48 | :non-standard-jsdoc :off} 49 | :pretty-print false}} 50 | {:id "dev" 51 | :source-paths ["src-cljs/dakait"] 52 | :compiler {:output-to "resources/public/js/index.js" 53 | :output-dir "resources/public/js/out" 54 | :source-map true 55 | :optimizations :none 56 | :pretty-print true}} 57 | ]}) 58 | -------------------------------------------------------------------------------- /resources/libs/bootstrap/bootstrap.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v3.0.3 (http://getbootstrap.com) 3 | * Copyright 2013 Twitter, Inc. 4 | * Licensed under http://www.apache.org/licenses/LICENSE-2.0 5 | */ 6 | 7 | if("undefined"==typeof jQuery)throw new Error("Bootstrap requires jQuery");+function(a){"use strict";function b(){var a=document.createElement("bootstrap"),b={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd otransitionend",transition:"transitionend"};for(var c in b)if(void 0!==a.style[c])return{end:b[c]}}a.fn.emulateTransitionEnd=function(b){var c=!1,d=this;a(this).one(a.support.transition.end,function(){c=!0});var e=function(){c||a(d).trigger(a.support.transition.end)};return setTimeout(e,b),this},a(function(){a.support.transition=b()})}(jQuery),+function(a){"use strict";var b='[data-dismiss="alert"]',c=function(c){a(c).on("click",b,this.close)};c.prototype.close=function(b){function c(){f.trigger("closed.bs.alert").remove()}var d=a(this),e=d.attr("data-target");e||(e=d.attr("href"),e=e&&e.replace(/.*(?=#[^\s]*$)/,""));var f=a(e);b&&b.preventDefault(),f.length||(f=d.hasClass("alert")?d:d.parent()),f.trigger(b=a.Event("close.bs.alert")),b.isDefaultPrevented()||(f.removeClass("in"),a.support.transition&&f.hasClass("fade")?f.one(a.support.transition.end,c).emulateTransitionEnd(150):c())};var d=a.fn.alert;a.fn.alert=function(b){return this.each(function(){var d=a(this),e=d.data("bs.alert");e||d.data("bs.alert",e=new c(this)),"string"==typeof b&&e[b].call(d)})},a.fn.alert.Constructor=c,a.fn.alert.noConflict=function(){return a.fn.alert=d,this},a(document).on("click.bs.alert.data-api",b,c.prototype.close)}(jQuery),+function(a){"use strict";var b=function(c,d){this.$element=a(c),this.options=a.extend({},b.DEFAULTS,d)};b.DEFAULTS={loadingText:"loading..."},b.prototype.setState=function(a){var b="disabled",c=this.$element,d=c.is("input")?"val":"html",e=c.data();a+="Text",e.resetText||c.data("resetText",c[d]()),c[d](e[a]||this.options[a]),setTimeout(function(){"loadingText"==a?c.addClass(b).attr(b,b):c.removeClass(b).removeAttr(b)},0)},b.prototype.toggle=function(){var a=this.$element.closest('[data-toggle="buttons"]'),b=!0;if(a.length){var c=this.$element.find("input");"radio"===c.prop("type")&&(c.prop("checked")&&this.$element.hasClass("active")?b=!1:a.find(".active").removeClass("active")),b&&c.prop("checked",!this.$element.hasClass("active")).trigger("change")}b&&this.$element.toggleClass("active")};var c=a.fn.button;a.fn.button=function(c){return this.each(function(){var d=a(this),e=d.data("bs.button"),f="object"==typeof c&&c;e||d.data("bs.button",e=new b(this,f)),"toggle"==c?e.toggle():c&&e.setState(c)})},a.fn.button.Constructor=b,a.fn.button.noConflict=function(){return a.fn.button=c,this},a(document).on("click.bs.button.data-api","[data-toggle^=button]",function(b){var c=a(b.target);c.hasClass("btn")||(c=c.closest(".btn")),c.button("toggle"),b.preventDefault()})}(jQuery),+function(a){"use strict";var b=function(b,c){this.$element=a(b),this.$indicators=this.$element.find(".carousel-indicators"),this.options=c,this.paused=this.sliding=this.interval=this.$active=this.$items=null,"hover"==this.options.pause&&this.$element.on("mouseenter",a.proxy(this.pause,this)).on("mouseleave",a.proxy(this.cycle,this))};b.DEFAULTS={interval:5e3,pause:"hover",wrap:!0},b.prototype.cycle=function(b){return b||(this.paused=!1),this.interval&&clearInterval(this.interval),this.options.interval&&!this.paused&&(this.interval=setInterval(a.proxy(this.next,this),this.options.interval)),this},b.prototype.getActiveIndex=function(){return this.$active=this.$element.find(".item.active"),this.$items=this.$active.parent().children(),this.$items.index(this.$active)},b.prototype.to=function(b){var c=this,d=this.getActiveIndex();return b>this.$items.length-1||0>b?void 0:this.sliding?this.$element.one("slid.bs.carousel",function(){c.to(b)}):d==b?this.pause().cycle():this.slide(b>d?"next":"prev",a(this.$items[b]))},b.prototype.pause=function(b){return b||(this.paused=!0),this.$element.find(".next, .prev").length&&a.support.transition.end&&(this.$element.trigger(a.support.transition.end),this.cycle(!0)),this.interval=clearInterval(this.interval),this},b.prototype.next=function(){return this.sliding?void 0:this.slide("next")},b.prototype.prev=function(){return this.sliding?void 0:this.slide("prev")},b.prototype.slide=function(b,c){var d=this.$element.find(".item.active"),e=c||d[b](),f=this.interval,g="next"==b?"left":"right",h="next"==b?"first":"last",i=this;if(!e.length){if(!this.options.wrap)return;e=this.$element.find(".item")[h]()}this.sliding=!0,f&&this.pause();var j=a.Event("slide.bs.carousel",{relatedTarget:e[0],direction:g});if(!e.hasClass("active")){if(this.$indicators.length&&(this.$indicators.find(".active").removeClass("active"),this.$element.one("slid.bs.carousel",function(){var b=a(i.$indicators.children()[i.getActiveIndex()]);b&&b.addClass("active")})),a.support.transition&&this.$element.hasClass("slide")){if(this.$element.trigger(j),j.isDefaultPrevented())return;e.addClass(b),e[0].offsetWidth,d.addClass(g),e.addClass(g),d.one(a.support.transition.end,function(){e.removeClass([b,g].join(" ")).addClass("active"),d.removeClass(["active",g].join(" ")),i.sliding=!1,setTimeout(function(){i.$element.trigger("slid.bs.carousel")},0)}).emulateTransitionEnd(600)}else{if(this.$element.trigger(j),j.isDefaultPrevented())return;d.removeClass("active"),e.addClass("active"),this.sliding=!1,this.$element.trigger("slid.bs.carousel")}return f&&this.cycle(),this}};var c=a.fn.carousel;a.fn.carousel=function(c){return this.each(function(){var d=a(this),e=d.data("bs.carousel"),f=a.extend({},b.DEFAULTS,d.data(),"object"==typeof c&&c),g="string"==typeof c?c:f.slide;e||d.data("bs.carousel",e=new b(this,f)),"number"==typeof c?e.to(c):g?e[g]():f.interval&&e.pause().cycle()})},a.fn.carousel.Constructor=b,a.fn.carousel.noConflict=function(){return a.fn.carousel=c,this},a(document).on("click.bs.carousel.data-api","[data-slide], [data-slide-to]",function(b){var c,d=a(this),e=a(d.attr("data-target")||(c=d.attr("href"))&&c.replace(/.*(?=#[^\s]+$)/,"")),f=a.extend({},e.data(),d.data()),g=d.attr("data-slide-to");g&&(f.interval=!1),e.carousel(f),(g=d.attr("data-slide-to"))&&e.data("bs.carousel").to(g),b.preventDefault()}),a(window).on("load",function(){a('[data-ride="carousel"]').each(function(){var b=a(this);b.carousel(b.data())})})}(jQuery),+function(a){"use strict";var b=function(c,d){this.$element=a(c),this.options=a.extend({},b.DEFAULTS,d),this.transitioning=null,this.options.parent&&(this.$parent=a(this.options.parent)),this.options.toggle&&this.toggle()};b.DEFAULTS={toggle:!0},b.prototype.dimension=function(){var a=this.$element.hasClass("width");return a?"width":"height"},b.prototype.show=function(){if(!this.transitioning&&!this.$element.hasClass("in")){var b=a.Event("show.bs.collapse");if(this.$element.trigger(b),!b.isDefaultPrevented()){var c=this.$parent&&this.$parent.find("> .panel > .in");if(c&&c.length){var d=c.data("bs.collapse");if(d&&d.transitioning)return;c.collapse("hide"),d||c.data("bs.collapse",null)}var e=this.dimension();this.$element.removeClass("collapse").addClass("collapsing")[e](0),this.transitioning=1;var f=function(){this.$element.removeClass("collapsing").addClass("in")[e]("auto"),this.transitioning=0,this.$element.trigger("shown.bs.collapse")};if(!a.support.transition)return f.call(this);var g=a.camelCase(["scroll",e].join("-"));this.$element.one(a.support.transition.end,a.proxy(f,this)).emulateTransitionEnd(350)[e](this.$element[0][g])}}},b.prototype.hide=function(){if(!this.transitioning&&this.$element.hasClass("in")){var b=a.Event("hide.bs.collapse");if(this.$element.trigger(b),!b.isDefaultPrevented()){var c=this.dimension();this.$element[c](this.$element[c]())[0].offsetHeight,this.$element.addClass("collapsing").removeClass("collapse").removeClass("in"),this.transitioning=1;var d=function(){this.transitioning=0,this.$element.trigger("hidden.bs.collapse").removeClass("collapsing").addClass("collapse")};return a.support.transition?(this.$element[c](0).one(a.support.transition.end,a.proxy(d,this)).emulateTransitionEnd(350),void 0):d.call(this)}}},b.prototype.toggle=function(){this[this.$element.hasClass("in")?"hide":"show"]()};var c=a.fn.collapse;a.fn.collapse=function(c){return this.each(function(){var d=a(this),e=d.data("bs.collapse"),f=a.extend({},b.DEFAULTS,d.data(),"object"==typeof c&&c);e||d.data("bs.collapse",e=new b(this,f)),"string"==typeof c&&e[c]()})},a.fn.collapse.Constructor=b,a.fn.collapse.noConflict=function(){return a.fn.collapse=c,this},a(document).on("click.bs.collapse.data-api","[data-toggle=collapse]",function(b){var c,d=a(this),e=d.attr("data-target")||b.preventDefault()||(c=d.attr("href"))&&c.replace(/.*(?=#[^\s]+$)/,""),f=a(e),g=f.data("bs.collapse"),h=g?"toggle":d.data(),i=d.attr("data-parent"),j=i&&a(i);g&&g.transitioning||(j&&j.find('[data-toggle=collapse][data-parent="'+i+'"]').not(d).addClass("collapsed"),d[f.hasClass("in")?"addClass":"removeClass"]("collapsed")),f.collapse(h)})}(jQuery),+function(a){"use strict";function b(){a(d).remove(),a(e).each(function(b){var d=c(a(this));d.hasClass("open")&&(d.trigger(b=a.Event("hide.bs.dropdown")),b.isDefaultPrevented()||d.removeClass("open").trigger("hidden.bs.dropdown"))})}function c(b){var c=b.attr("data-target");c||(c=b.attr("href"),c=c&&/#/.test(c)&&c.replace(/.*(?=#[^\s]*$)/,""));var d=c&&a(c);return d&&d.length?d:b.parent()}var d=".dropdown-backdrop",e="[data-toggle=dropdown]",f=function(b){a(b).on("click.bs.dropdown",this.toggle)};f.prototype.toggle=function(d){var e=a(this);if(!e.is(".disabled, :disabled")){var f=c(e),g=f.hasClass("open");if(b(),!g){if("ontouchstart"in document.documentElement&&!f.closest(".navbar-nav").length&&a(''}),b.prototype=a.extend({},a.fn.tooltip.Constructor.prototype),b.prototype.constructor=b,b.prototype.getDefaults=function(){return b.DEFAULTS},b.prototype.setContent=function(){var a=this.tip(),b=this.getTitle(),c=this.getContent();a.find(".popover-title")[this.options.html?"html":"text"](b),a.find(".popover-content")[this.options.html?"html":"text"](c),a.removeClass("fade top bottom left right in"),a.find(".popover-title").html()||a.find(".popover-title").hide()},b.prototype.hasContent=function(){return this.getTitle()||this.getContent()},b.prototype.getContent=function(){var a=this.$element,b=this.options;return a.attr("data-content")||("function"==typeof b.content?b.content.call(a[0]):b.content)},b.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".arrow")},b.prototype.tip=function(){return this.$tip||(this.$tip=a(this.options.template)),this.$tip};var c=a.fn.popover;a.fn.popover=function(c){return this.each(function(){var d=a(this),e=d.data("bs.popover"),f="object"==typeof c&&c;e||d.data("bs.popover",e=new b(this,f)),"string"==typeof c&&e[c]()})},a.fn.popover.Constructor=b,a.fn.popover.noConflict=function(){return a.fn.popover=c,this}}(jQuery),+function(a){"use strict";function b(c,d){var e,f=a.proxy(this.process,this);this.$element=a(c).is("body")?a(window):a(c),this.$body=a("body"),this.$scrollElement=this.$element.on("scroll.bs.scroll-spy.data-api",f),this.options=a.extend({},b.DEFAULTS,d),this.selector=(this.options.target||(e=a(c).attr("href"))&&e.replace(/.*(?=#[^\s]+$)/,"")||"")+" .nav li > a",this.offsets=a([]),this.targets=a([]),this.activeTarget=null,this.refresh(),this.process()}b.DEFAULTS={offset:10},b.prototype.refresh=function(){var b=this.$element[0]==window?"offset":"position";this.offsets=a([]),this.targets=a([]);var c=this;this.$body.find(this.selector).map(function(){var d=a(this),e=d.data("target")||d.attr("href"),f=/^#\w/.test(e)&&a(e);return f&&f.length&&[[f[b]().top+(!a.isWindow(c.$scrollElement.get(0))&&c.$scrollElement.scrollTop()),e]]||null}).sort(function(a,b){return a[0]-b[0]}).each(function(){c.offsets.push(this[0]),c.targets.push(this[1])})},b.prototype.process=function(){var a,b=this.$scrollElement.scrollTop()+this.options.offset,c=this.$scrollElement[0].scrollHeight||this.$body[0].scrollHeight,d=c-this.$scrollElement.height(),e=this.offsets,f=this.targets,g=this.activeTarget;if(b>=d)return g!=(a=f.last()[0])&&this.activate(a);for(a=e.length;a--;)g!=f[a]&&b>=e[a]&&(!e[a+1]||b<=e[a+1])&&this.activate(f[a])},b.prototype.activate=function(b){this.activeTarget=b,a(this.selector).parents(".active").removeClass("active");var c=this.selector+'[data-target="'+b+'"],'+this.selector+'[href="'+b+'"]',d=a(c).parents("li").addClass("active");d.parent(".dropdown-menu").length&&(d=d.closest("li.dropdown").addClass("active")),d.trigger("activate.bs.scrollspy")};var c=a.fn.scrollspy;a.fn.scrollspy=function(c){return this.each(function(){var d=a(this),e=d.data("bs.scrollspy"),f="object"==typeof c&&c;e||d.data("bs.scrollspy",e=new b(this,f)),"string"==typeof c&&e[c]()})},a.fn.scrollspy.Constructor=b,a.fn.scrollspy.noConflict=function(){return a.fn.scrollspy=c,this},a(window).on("load",function(){a('[data-spy="scroll"]').each(function(){var b=a(this);b.scrollspy(b.data())})})}(jQuery),+function(a){"use strict";var b=function(b){this.element=a(b)};b.prototype.show=function(){var b=this.element,c=b.closest("ul:not(.dropdown-menu)"),d=b.data("target");if(d||(d=b.attr("href"),d=d&&d.replace(/.*(?=#[^\s]*$)/,"")),!b.parent("li").hasClass("active")){var e=c.find(".active:last a")[0],f=a.Event("show.bs.tab",{relatedTarget:e});if(b.trigger(f),!f.isDefaultPrevented()){var g=a(d);this.activate(b.parent("li"),c),this.activate(g,g.parent(),function(){b.trigger({type:"shown.bs.tab",relatedTarget:e})})}}},b.prototype.activate=function(b,c,d){function e(){f.removeClass("active").find("> .dropdown-menu > .active").removeClass("active"),b.addClass("active"),g?(b[0].offsetWidth,b.addClass("in")):b.removeClass("fade"),b.parent(".dropdown-menu")&&b.closest("li.dropdown").addClass("active"),d&&d()}var f=c.find("> .active"),g=d&&a.support.transition&&f.hasClass("fade");g?f.one(a.support.transition.end,e).emulateTransitionEnd(150):e(),f.removeClass("in")};var c=a.fn.tab;a.fn.tab=function(c){return this.each(function(){var d=a(this),e=d.data("bs.tab");e||d.data("bs.tab",e=new b(this)),"string"==typeof c&&e[c]()})},a.fn.tab.Constructor=b,a.fn.tab.noConflict=function(){return a.fn.tab=c,this},a(document).on("click.bs.tab.data-api",'[data-toggle="tab"], [data-toggle="pill"]',function(b){b.preventDefault(),a(this).tab("show")})}(jQuery),+function(a){"use strict";var b=function(c,d){this.options=a.extend({},b.DEFAULTS,d),this.$window=a(window).on("scroll.bs.affix.data-api",a.proxy(this.checkPosition,this)).on("click.bs.affix.data-api",a.proxy(this.checkPositionWithEventLoop,this)),this.$element=a(c),this.affixed=this.unpin=null,this.checkPosition()};b.RESET="affix affix-top affix-bottom",b.DEFAULTS={offset:0},b.prototype.checkPositionWithEventLoop=function(){setTimeout(a.proxy(this.checkPosition,this),1)},b.prototype.checkPosition=function(){if(this.$element.is(":visible")){var c=a(document).height(),d=this.$window.scrollTop(),e=this.$element.offset(),f=this.options.offset,g=f.top,h=f.bottom;"object"!=typeof f&&(h=g=f),"function"==typeof g&&(g=f.top()),"function"==typeof h&&(h=f.bottom());var i=null!=this.unpin&&d+this.unpin<=e.top?!1:null!=h&&e.top+this.$element.height()>=c-h?"bottom":null!=g&&g>=d?"top":!1;this.affixed!==i&&(this.unpin&&this.$element.css("top",""),this.affixed=i,this.unpin="bottom"==i?e.top-d:null,this.$element.removeClass(b.RESET).addClass("affix"+(i?"-"+i:"")),"bottom"==i&&this.$element.offset({top:document.body.offsetHeight-h-this.$element.height()}))}};var c=a.fn.affix;a.fn.affix=function(c){return this.each(function(){var d=a(this),e=d.data("bs.affix"),f="object"==typeof c&&c;e||d.data("bs.affix",e=new b(this,f)),"string"==typeof c&&e[c]()})},a.fn.affix.Constructor=b,a.fn.affix.noConflict=function(){return a.fn.affix=c,this},a(window).on("load",function(){a('[data-spy="affix"]').each(function(){var b=a(this),c=b.data();c.offset=c.offset||{},c.offsetBottom&&(c.offset.bottom=c.offsetBottom),c.offsetTop&&(c.offset.top=c.offsetTop),b.affix(c)})})}(jQuery); -------------------------------------------------------------------------------- /resources/libs/react/react.externs.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Externs for React 0.10.0. 3 | * 4 | * @see http://facebook.github.io/react 5 | * @externs 6 | */ 7 | 8 | /** 9 | * @type {Object} 10 | * @const 11 | */ 12 | var React = {}; 13 | 14 | /** 15 | * @type {string} 16 | */ 17 | React.version; 18 | 19 | /** 20 | * @param {boolean} shouldUseTouch 21 | */ 22 | React.initializeTouchEvents = function(shouldUseTouch) {}; 23 | 24 | /** 25 | * @param {Object} specification 26 | * @return {function( 27 | Object=, 28 | (string|React.ReactComponent|Array.)= 29 | ): React.ReactComponent} 30 | */ 31 | React.createClass = function(specification) {}; 32 | 33 | /** 34 | * @param {*} componentClass 35 | * @return {boolean} 36 | */ 37 | React.isValidClass = function(componentClass) {}; 38 | 39 | /** 40 | * @param {React.ReactComponent} container 41 | * @param {Element} mountPoint 42 | * @param {Function=} callback 43 | * @return {React.ReactComponent} 44 | */ 45 | React.renderComponent = function(container, mountPoint, callback) {}; 46 | 47 | /** 48 | * @param {Element} container 49 | */ 50 | React.unmountComponentAtNode = function(container) {}; 51 | 52 | /** 53 | * @param {React.ReactComponent} component 54 | * @return {string} 55 | */ 56 | React.renderComponentToString = function(component) {}; 57 | 58 | /** 59 | * @param {React.ReactComponent} component 60 | * @return {string} 61 | */ 62 | React.renderComponentToStaticMarkup = function(component) {}; 63 | 64 | /** 65 | * Constructs a component instance of `constructor` with `initialProps` and 66 | * renders it into the supplied `container`. 67 | * 68 | * @param {Function} constructor React component constructor. 69 | * @param {Object} props Initial props of the component instance. 70 | * @param {Element} container DOM element to render into. 71 | * @return {React.ReactComponent} Component instance rendered in `container`. 72 | */ 73 | React.constructAndRenderComponent = function(constructor, props, container) {}; 74 | 75 | /** 76 | * Constructs a component instance of `constructor` with `initialProps` and 77 | * renders it into a container node identified by supplied `id`. 78 | * 79 | * @param {Function} componentConstructor React component constructor 80 | * @param {Object} props Initial props of the component instance. 81 | * @param {string} id ID of the DOM element to render into. 82 | * @return {React.ReactComponent} Component instance rendered in the container node. 83 | */ 84 | React.constructAndRenderComponentByID = function(componentConstructor, props, id) {}; 85 | 86 | /** 87 | * @interface 88 | */ 89 | React.ReactComponent = function() {}; 90 | 91 | /** 92 | * @type {Object} 93 | */ 94 | React.ReactComponent.prototype.props; 95 | 96 | /** 97 | * @type {Object} 98 | */ 99 | React.ReactComponent.prototype.state; 100 | 101 | /** 102 | * @type {Object} 103 | */ 104 | React.ReactComponent.prototype.refs; 105 | 106 | /** 107 | * @type {Object} 108 | */ 109 | React.ReactComponent.prototype.context; 110 | 111 | /** 112 | * @type {Object} 113 | * @protected 114 | */ 115 | React.ReactComponent.prototype.propTypes; 116 | 117 | /** 118 | * @type {Object} 119 | * @protected 120 | */ 121 | React.ReactComponent.prototype.contextTypes; 122 | 123 | /** 124 | * @param {Object} nextProps 125 | * @param {Function=} callback 126 | */ 127 | React.ReactComponent.prototype.setProps = function(nextProps, callback) {}; 128 | 129 | /** 130 | * @return {Object} 131 | */ 132 | React.ReactComponent.prototype.getInitialState = function() {}; 133 | 134 | /** 135 | * @return {Object} 136 | */ 137 | React.ReactComponent.prototype.getDefaultProps = function() {}; 138 | 139 | /** 140 | * @return {Object} 141 | */ 142 | React.ReactComponent.prototype.getChildContext = function() {}; 143 | 144 | /** 145 | * @return {Element} 146 | */ 147 | React.ReactComponent.prototype.getDOMNode = function() {}; 148 | 149 | /** 150 | * @param {Object} nextProps 151 | * @param {Function=} callback 152 | */ 153 | React.ReactComponent.prototype.replaceProps = function(nextProps, callback) {}; 154 | 155 | /** 156 | * @param {React.ReactComponent} targetComponent 157 | * @return {React.ReactComponent} 158 | */ 159 | React.ReactComponent.prototype.transferPropsTo = function(targetComponent) {}; 160 | 161 | /** 162 | * @param {Function=} callback 163 | */ 164 | React.ReactComponent.prototype.forceUpdate = function(callback) {}; 165 | 166 | /** 167 | * @param {Object} nextState 168 | * @param {Function=} callback 169 | */ 170 | React.ReactComponent.prototype.setState = function(nextState, callback) {}; 171 | 172 | /** 173 | * @param {Object} nextState 174 | * @param {Function=} callback 175 | */ 176 | React.ReactComponent.prototype.replaceState = function(nextState, callback) {}; 177 | 178 | /** 179 | * @protected 180 | */ 181 | React.ReactComponent.prototype.componentWillMount = function() {}; 182 | 183 | /** 184 | * @param {Element} element 185 | * @protected 186 | */ 187 | React.ReactComponent.prototype.componentDidMount = function(element) {}; 188 | 189 | /** 190 | * @param {Object} nextProps 191 | * @protected 192 | */ 193 | React.ReactComponent.prototype.componentWillReceiveProps = function( 194 | nextProps) {}; 195 | 196 | /** 197 | * @param {Object} nextProps 198 | * @param {Object} nextState 199 | * @return {boolean} 200 | * @protected 201 | */ 202 | React.ReactComponent.prototype.shouldComponentUpdate = function( 203 | nextProps, nextState) {}; 204 | 205 | /** 206 | * @param {Object} nextProps 207 | * @param {Object} nextState 208 | * @protected 209 | */ 210 | React.ReactComponent.prototype.componentWillUpdate = function( 211 | nextProps, nextState) {}; 212 | 213 | /** 214 | * @param {Object} prevProps 215 | * @param {Object} prevState 216 | * @param {Element} rootNode 217 | * @protected 218 | */ 219 | React.ReactComponent.prototype.componentDidUpdate = function( 220 | prevProps, prevState, rootNode) {}; 221 | 222 | /** 223 | * @protected 224 | */ 225 | React.ReactComponent.prototype.componentWillUnmount = function() {}; 226 | 227 | /** 228 | * @return {React.ReactComponent} 229 | * @protected 230 | */ 231 | React.ReactComponent.prototype.render = function() {}; 232 | 233 | /** 234 | * Interface to preserve React attributes for advanced compilation. 235 | * @interface 236 | */ 237 | React.ReactAtrribute = function() {}; 238 | 239 | /** 240 | * @type {Object} 241 | */ 242 | React.ReactAtrribute.dangerouslySetInnerHTML; 243 | 244 | /** 245 | * @type {string} 246 | */ 247 | React.ReactAtrribute.__html; 248 | 249 | /** 250 | * @type {string} 251 | */ 252 | React.ReactAtrribute.key; 253 | 254 | /** 255 | * @type {string} 256 | */ 257 | React.ReactAtrribute.ref; 258 | 259 | // http://facebook.github.io/react/docs/events.html 260 | 261 | /** 262 | * @type {Function} 263 | */ 264 | React.ReactAtrribute.onCopy; 265 | 266 | /** 267 | * @type {Function} 268 | */ 269 | React.ReactAtrribute.onCut; 270 | 271 | /** 272 | * @type {Function} 273 | */ 274 | React.ReactAtrribute.onPaste; 275 | 276 | /** 277 | * @type {Function} 278 | */ 279 | React.ReactAtrribute.onKeyDown; 280 | 281 | /** 282 | * @type {Function} 283 | */ 284 | React.ReactAtrribute.onKeyPress; 285 | 286 | /** 287 | * @type {Function} 288 | */ 289 | React.ReactAtrribute.onKeyUp; 290 | 291 | /** 292 | * @type {Function} 293 | */ 294 | React.ReactAtrribute.onFocus; 295 | 296 | /** 297 | * @type {Function} 298 | */ 299 | React.ReactAtrribute.onBlur; 300 | 301 | /** 302 | * @type {Function} 303 | */ 304 | React.ReactAtrribute.onChange; 305 | 306 | /** 307 | * @type {Function} 308 | */ 309 | React.ReactAtrribute.onInput; 310 | 311 | /** 312 | * @type {Function} 313 | */ 314 | React.ReactAtrribute.onSubmit; 315 | 316 | /** 317 | * @type {Function} 318 | */ 319 | React.ReactAtrribute.onClick; 320 | 321 | /** 322 | * @type {Function} 323 | */ 324 | React.ReactAtrribute.onDoubleClick; 325 | 326 | /** 327 | * @type {Function} 328 | */ 329 | React.ReactAtrribute.onDrag; 330 | 331 | /** 332 | * @type {Function} 333 | */ 334 | React.ReactAtrribute.onDragEnd; 335 | 336 | /** 337 | * @type {Function} 338 | */ 339 | React.ReactAtrribute.onDragEnter; 340 | 341 | /** 342 | * @type {Function} 343 | */ 344 | React.ReactAtrribute.onDragExit; 345 | 346 | /** 347 | * @type {Function} 348 | */ 349 | React.ReactAtrribute.onDragLeave; 350 | 351 | /** 352 | * @type {Function} 353 | */ 354 | React.ReactAtrribute.onDragOver; 355 | 356 | /** 357 | * @type {Function} 358 | */ 359 | React.ReactAtrribute.onDragStart; 360 | 361 | /** 362 | * @type {Function} 363 | */ 364 | React.ReactAtrribute.onDrop; 365 | 366 | /** 367 | * @type {Function} 368 | */ 369 | React.ReactAtrribute.onMouseDown; 370 | 371 | /** 372 | * @type {Function} 373 | */ 374 | React.ReactAtrribute.onMouseEnter; 375 | 376 | /** 377 | * @type {Function} 378 | */ 379 | React.ReactAtrribute.onMouseLeave; 380 | 381 | /** 382 | * @type {Function} 383 | */ 384 | React.ReactAtrribute.onMouseMove; 385 | 386 | /** 387 | * @type {Function} 388 | */ 389 | React.ReactAtrribute.onMouseUp; 390 | 391 | /** 392 | * @type {Function} 393 | */ 394 | React.ReactAtrribute.onTouchCancel; 395 | 396 | /** 397 | * @type {Function} 398 | */ 399 | React.ReactAtrribute.onTouchEnd; 400 | 401 | /** 402 | * @type {Function} 403 | */ 404 | React.ReactAtrribute.onTouchMove; 405 | 406 | /** 407 | * @type {Function} 408 | */ 409 | React.ReactAtrribute.onTouchStart; 410 | 411 | /** 412 | * @type {Function} 413 | */ 414 | React.ReactAtrribute.onScroll; 415 | 416 | /** 417 | * @type {Function} 418 | */ 419 | React.ReactAtrribute.onWheel; 420 | 421 | /** 422 | * @type {Object} 423 | * @const 424 | */ 425 | React.DOM = {}; 426 | 427 | /** 428 | * @param {Object=} props 429 | * @param {...string|React.ReactComponent|Array.|boolean} children 430 | * @return {React.ReactComponent} 431 | * @protected 432 | */ 433 | React.DOM.a = function(props, children) {}; 434 | 435 | /** 436 | * @param {Object=} props 437 | * @param {...string|React.ReactComponent|Array.|boolean} children 438 | * @return {React.ReactComponent} 439 | * @protected 440 | */ 441 | React.DOM.article = function(props, children) {}; 442 | 443 | /** 444 | * @param {Object=} props 445 | * @param {...string|React.ReactComponent|Array.|boolean} children 446 | * @return {React.ReactComponent} 447 | * @protected 448 | */ 449 | React.DOM.abbr = function(props, children) {}; 450 | 451 | /** 452 | * @param {Object=} props 453 | * @param {...string|React.ReactComponent|Array.|boolean} children 454 | * @return {React.ReactComponent} 455 | * @protected 456 | */ 457 | React.DOM.address = function(props, children) {}; 458 | 459 | /** 460 | * @param {Object=} props 461 | * @param {...string|React.ReactComponent|Array.|boolean} children 462 | * @return {React.ReactComponent} 463 | * @protected 464 | */ 465 | React.DOM.audio = function(props, children) {}; 466 | 467 | /** 468 | * @param {Object=} props 469 | * @param {...string|React.ReactComponent|Array.|boolean} children 470 | * @return {React.ReactComponent} 471 | * @protected 472 | */ 473 | React.DOM.b = function(props, children) {}; 474 | 475 | /** 476 | * @param {Object=} props 477 | * @param {...string|React.ReactComponent|Array.|boolean} children 478 | * @return {React.ReactComponent} 479 | * @protected 480 | */ 481 | React.DOM.body = function(props, children) {}; 482 | 483 | /** 484 | * @param {Object=} props 485 | * @param {...string|React.ReactComponent|Array.|boolean} children 486 | * @return {React.ReactComponent} 487 | * @protected 488 | */ 489 | React.DOM.br = function(props, children) {}; 490 | 491 | /** 492 | * @param {Object=} props 493 | * @param {...string|React.ReactComponent|Array.|boolean} children 494 | * @return {React.ReactComponent} 495 | * @protected 496 | */ 497 | React.DOM.button = function(props, children) {}; 498 | 499 | /** 500 | * @param {Object=} props 501 | * @param {...string|React.ReactComponent|Array.|boolean} children 502 | * @return {React.ReactComponent} 503 | * @protected 504 | */ 505 | React.DOM.code = function(props, children) {}; 506 | 507 | /** 508 | * @param {Object=} props 509 | * @param {...string|React.ReactComponent|Array.|boolean} children 510 | * @return {React.ReactComponent} 511 | * @protected 512 | */ 513 | React.DOM.col = function(props, children) {}; 514 | 515 | /** 516 | * @param {Object=} props 517 | * @param {...string|React.ReactComponent|Array.|boolean} children 518 | * @return {React.ReactComponent} 519 | * @protected 520 | */ 521 | React.DOM.colgroup = function(props, children) {}; 522 | 523 | /** 524 | * @param {Object=} props 525 | * @param {...string|React.ReactComponent|Array.|boolean} children 526 | * @return {React.ReactComponent} 527 | * @protected 528 | */ 529 | React.DOM.dd = function(props, children) {}; 530 | 531 | /** 532 | * @param {Object=} props 533 | * @param {...string|React.ReactComponent|Array.|boolean} children 534 | * @return {React.ReactComponent} 535 | * @protected 536 | */ 537 | React.DOM.div = function(props, children) {}; 538 | 539 | /** 540 | * @param {Object=} props 541 | * @param {...string|React.ReactComponent|Array.|boolean} children 542 | * @return {React.ReactComponent} 543 | * @protected 544 | */ 545 | React.DOM.section = function(props, children) {}; 546 | 547 | /** 548 | * @param {Object=} props 549 | * @param {...string|React.ReactComponent|Array.|boolean} children 550 | * @return {React.ReactComponent} 551 | * @protected 552 | */ 553 | React.DOM.dl = function(props, children) {}; 554 | 555 | /** 556 | * @param {Object=} props 557 | * @param {...string|React.ReactComponent|Array.|boolean} children 558 | * @return {React.ReactComponent} 559 | * @protected 560 | */ 561 | React.DOM.dt = function(props, children) {}; 562 | 563 | /** 564 | * @param {Object=} props 565 | * @param {...string|React.ReactComponent|Array.|boolean} children 566 | * @return {React.ReactComponent} 567 | * @protected 568 | */ 569 | React.DOM.em = function(props, children) {}; 570 | 571 | /** 572 | * @param {Object=} props 573 | * @param {...string|React.ReactComponent|Array.|boolean} children 574 | * @return {React.ReactComponent} 575 | * @protected 576 | */ 577 | React.DOM.embed = function(props, children) {}; 578 | 579 | /** 580 | * @param {Object=} props 581 | * @param {...string|React.ReactComponent|Array.|boolean} children 582 | * @return {React.ReactComponent} 583 | * @protected 584 | */ 585 | React.DOM.fieldset = function(props, children) {}; 586 | 587 | /** 588 | * @param {Object=} props 589 | * @param {...string|React.ReactComponent|Array.|boolean} children 590 | * @return {React.ReactComponent} 591 | * @protected 592 | */ 593 | React.DOM.footer = function(props, children) {}; 594 | 595 | /** 596 | * @param {Object=} props 597 | * @param {...string|React.ReactComponent|Array.|boolean} children 598 | * @return {React.ReactComponent} 599 | * @protected 600 | */ 601 | React.DOM.form = function(props, children) {}; 602 | 603 | /** 604 | * @param {Object=} props 605 | * @param {...string|React.ReactComponent|Array.|boolean} children 606 | * @return {React.ReactComponent} 607 | * @protected 608 | */ 609 | React.DOM.h1 = function(props, children) {}; 610 | 611 | /** 612 | * @param {Object=} props 613 | * @param {...string|React.ReactComponent|Array.|boolean} children 614 | * @return {React.ReactComponent} 615 | * @protected 616 | */ 617 | React.DOM.h2 = function(props, children) {}; 618 | 619 | /** 620 | * @param {Object=} props 621 | * @param {...string|React.ReactComponent|Array.|boolean} children 622 | * @return {React.ReactComponent} 623 | * @protected 624 | */ 625 | React.DOM.h3 = function(props, children) {}; 626 | 627 | /** 628 | * @param {Object=} props 629 | * @param {...string|React.ReactComponent|Array.|boolean} children 630 | * @return {React.ReactComponent} 631 | * @protected 632 | */ 633 | React.DOM.h4 = function(props, children) {}; 634 | 635 | /** 636 | * @param {Object=} props 637 | * @param {...string|React.ReactComponent|Array.|boolean} children 638 | * @return {React.ReactComponent} 639 | * @protected 640 | */ 641 | React.DOM.h5 = function(props, children) {}; 642 | 643 | /** 644 | * @param {Object=} props 645 | * @param {...string|React.ReactComponent|Array.|boolean} children 646 | * @return {React.ReactComponent} 647 | * @protected 648 | */ 649 | React.DOM.h6 = function(props, children) {}; 650 | 651 | /** 652 | * @param {Object=} props 653 | * @param {...string|React.ReactComponent|Array.|boolean} children 654 | * @return {React.ReactComponent} 655 | * @protected 656 | */ 657 | React.DOM.header = function(props, children) {}; 658 | 659 | /** 660 | * @param {Object=} props 661 | * @param {...string|React.ReactComponent|Array.|boolean} children 662 | * @return {React.ReactComponent} 663 | * @protected 664 | */ 665 | React.DOM.hr = function(props, children) {}; 666 | 667 | /** 668 | * @param {Object=} props 669 | * @param {...string|React.ReactComponent|Array.|boolean} children 670 | * @return {React.ReactComponent} 671 | * @protected 672 | */ 673 | React.DOM.i = function(props, children) {}; 674 | 675 | /** 676 | * @param {Object=} props 677 | * @param {...string|React.ReactComponent|Array.|boolean} children 678 | * @return {React.ReactComponent} 679 | * @protected 680 | */ 681 | React.DOM.iframe = function(props, children) {}; 682 | 683 | /** 684 | * @param {Object=} props 685 | * @param {...string|React.ReactComponent|Array.|boolean} children 686 | * @return {React.ReactComponent} 687 | * @protected 688 | */ 689 | React.DOM.img = function(props, children) {}; 690 | 691 | /** 692 | * @param {Object=} props 693 | * @param {...string|React.ReactComponent|Array.|boolean} children 694 | * @return {React.ReactComponent} 695 | * @protected 696 | */ 697 | React.DOM.input = function(props, children) {}; 698 | 699 | /** 700 | * @param {Object=} props 701 | * @param {...string|React.ReactComponent|Array.|boolean} children 702 | * @return {React.ReactComponent} 703 | * @protected 704 | */ 705 | React.DOM.label = function(props, children) {}; 706 | 707 | /** 708 | * @param {Object=} props 709 | * @param {...string|React.ReactComponent|Array.|boolean} children 710 | * @return {React.ReactComponent} 711 | * @protected 712 | */ 713 | React.DOM.legend = function(props, children) {}; 714 | 715 | /** 716 | * @param {Object=} props 717 | * @param {...string|React.ReactComponent|Array.|boolean} children 718 | * @return {React.ReactComponent} 719 | * @protected 720 | */ 721 | React.DOM.li = function(props, children) {}; 722 | 723 | /** 724 | * @param {Object=} props 725 | * @param {...string|React.ReactComponent|Array.|boolean} children 726 | * @return {React.ReactComponent} 727 | * @protected 728 | */ 729 | React.DOM.nav = function(props, children) {}; 730 | 731 | /** 732 | * @param {Object=} props 733 | * @param {...string|React.ReactComponent|Array.|boolean} children 734 | * @return {React.ReactComponent} 735 | * @protected 736 | */ 737 | React.DOM.object = function(props, children) {}; 738 | 739 | /** 740 | * @param {Object=} props 741 | * @param {...string|React.ReactComponent|Array.|boolean} children 742 | * @return {React.ReactComponent} 743 | * @protected 744 | */ 745 | React.DOM.ol = function(props, children) {}; 746 | 747 | /** 748 | * @param {Object=} props 749 | * @param {...string|React.ReactComponent|Array.|boolean} children 750 | * @return {React.ReactComponent} 751 | * @protected 752 | */ 753 | React.DOM.optgroup = function(props, children) {}; 754 | 755 | /** 756 | * @param {Object=} props 757 | * @param {...string|React.ReactComponent|Array.|boolean} children 758 | * @return {React.ReactComponent} 759 | * @protected 760 | */ 761 | React.DOM.option = function(props, children) {}; 762 | 763 | /** 764 | * @param {Object=} props 765 | * @param {...string|React.ReactComponent|Array.|boolean} children 766 | * @return {React.ReactComponent} 767 | * @protected 768 | */ 769 | React.DOM.p = function(props, children) {}; 770 | 771 | /** 772 | * @param {Object=} props 773 | * @param {...string|React.ReactComponent|Array.|boolean} children 774 | * @return {React.ReactComponent} 775 | * @protected 776 | */ 777 | React.DOM.param = function(props, children) {}; 778 | 779 | /** 780 | * @param {Object=} props 781 | * @param {...string|React.ReactComponent|Array.|boolean} children 782 | * @return {React.ReactComponent} 783 | * @protected 784 | */ 785 | React.DOM.pre = function(props, children) {}; 786 | 787 | /** 788 | * @param {Object=} props 789 | * @param {...string|React.ReactComponent|Array.|boolean} children 790 | * @return {React.ReactComponent} 791 | * @protected 792 | */ 793 | React.DOM.select = function(props, children) {}; 794 | 795 | /** 796 | * @param {Object=} props 797 | * @param {...string|React.ReactComponent|Array.|boolean} children 798 | * @return {React.ReactComponent} 799 | * @protected 800 | */ 801 | React.DOM.small = function(props, children) {}; 802 | 803 | /** 804 | * @param {Object=} props 805 | * @param {...string|React.ReactComponent|Array.|boolean} children 806 | * @return {React.ReactComponent} 807 | * @protected 808 | */ 809 | React.DOM.source = function(props, children) {}; 810 | 811 | /** 812 | * @param {Object=} props 813 | * @param {...string|React.ReactComponent|Array.|boolean} children 814 | * @return {React.ReactComponent} 815 | * @protected 816 | */ 817 | React.DOM.span = function(props, children) {}; 818 | 819 | /** 820 | * @param {Object=} props 821 | * @param {...string|React.ReactComponent|Array.|boolean} children 822 | * @return {React.ReactComponent} 823 | * @protected 824 | */ 825 | React.DOM.sub = function(props, children) {}; 826 | 827 | /** 828 | * @param {Object=} props 829 | * @param {...string|React.ReactComponent|Array.|boolean} children 830 | * @return {React.ReactComponent} 831 | * @protected 832 | */ 833 | React.DOM.sup = function(props, children) {}; 834 | 835 | /** 836 | * @param {Object=} props 837 | * @param {...string|React.ReactComponent|Array.|boolean} children 838 | * @return {React.ReactComponent} 839 | * @protected 840 | */ 841 | React.DOM.strong = function(props, children) {}; 842 | 843 | /** 844 | * @param {Object=} props 845 | * @param {...string|React.ReactComponent|Array.|boolean} children 846 | * @return {React.ReactComponent} 847 | * @protected 848 | */ 849 | React.DOM.table = function(props, children) {}; 850 | 851 | /** 852 | * @param {Object=} props 853 | * @param {...string|React.ReactComponent|Array.|boolean} children 854 | * @return {React.ReactComponent} 855 | * @protected 856 | */ 857 | React.DOM.tbody = function(props, children) {}; 858 | 859 | /** 860 | * @param {Object=} props 861 | * @param {...string|React.ReactComponent|Array.|boolean} children 862 | * @return {React.ReactComponent} 863 | * @protected 864 | */ 865 | React.DOM.td = function(props, children) {}; 866 | 867 | /** 868 | * @param {Object=} props 869 | * @param {...string|React.ReactComponent|Array.|boolean} children 870 | * @return {React.ReactComponent} 871 | * @protected 872 | */ 873 | React.DOM.textarea = function(props, children) {}; 874 | 875 | /** 876 | * @param {Object=} props 877 | * @param {...string|React.ReactComponent|Array.|boolean} children 878 | * @return {React.ReactComponent} 879 | * @protected 880 | */ 881 | React.DOM.tfoot = function(props, children) {}; 882 | 883 | /** 884 | * @param {Object=} props 885 | * @param {...string|React.ReactComponent|Array.|boolean} children 886 | * @return {React.ReactComponent} 887 | * @protected 888 | */ 889 | React.DOM.th = function(props, children) {}; 890 | 891 | /** 892 | * @param {Object=} props 893 | * @param {...string|React.ReactComponent|Array.|boolean} children 894 | * @return {React.ReactComponent} 895 | * @protected 896 | */ 897 | React.DOM.thead = function(props, children) {}; 898 | 899 | /** 900 | * @param {Object=} props 901 | * @param {...string|React.ReactComponent|Array.|boolean} children 902 | * @return {React.ReactComponent} 903 | * @protected 904 | */ 905 | React.DOM.time = function(props, children) {}; 906 | 907 | /** 908 | * @param {Object=} props 909 | * @param {...string|React.ReactComponent|Array.|boolean} children 910 | * @return {React.ReactComponent} 911 | * @protected 912 | */ 913 | React.DOM.title = function(props, children) {}; 914 | 915 | /** 916 | * @param {Object=} props 917 | * @param {...string|React.ReactComponent|Array.|boolean} children 918 | * @return {React.ReactComponent} 919 | * @protected 920 | */ 921 | React.DOM.tr = function(props, children) {}; 922 | 923 | /** 924 | * @param {Object=} props 925 | * @param {...string|React.ReactComponent|Array.|boolean} children 926 | * @return {React.ReactComponent} 927 | * @protected 928 | */ 929 | React.DOM.u = function(props, children) {}; 930 | 931 | /** 932 | * @param {Object=} props 933 | * @param {...string|React.ReactComponent|Array.|boolean} children 934 | * @return {React.ReactComponent} 935 | * @protected 936 | */ 937 | React.DOM.ul = function(props, children) {}; 938 | 939 | /** 940 | * @param {Object=} props 941 | * @param {...string|React.ReactComponent|Array.|boolean} children 942 | * @return {React.ReactComponent} 943 | * @protected 944 | */ 945 | React.DOM.video = function(props, children) {}; 946 | 947 | /** 948 | * @param {Object=} props 949 | * @param {...string|React.ReactComponent|Array.|boolean} children 950 | * @return {React.ReactComponent} 951 | * @protected 952 | */ 953 | React.DOM.wbr = function(props, children) {}; 954 | 955 | // SVG 956 | 957 | /** 958 | * @param {Object=} props 959 | * @param {...string|React.ReactComponent|Array.|boolean} children 960 | * @return {React.ReactComponent} 961 | * @protected 962 | */ 963 | React.DOM.circle = function(props, children) {}; 964 | 965 | /** 966 | * @param {Object=} props 967 | * @param {...string|React.ReactComponent|Array.|boolean} children 968 | * @return {React.ReactComponent} 969 | * @protected 970 | */ 971 | React.DOM.defs = function(props, children) {}; 972 | 973 | /** 974 | * @param {Object=} props 975 | * @param {...string|React.ReactComponent|Array.|boolean} children 976 | * @return {React.ReactComponent} 977 | * @protected 978 | */ 979 | React.DOM.g = function(props, children) {}; 980 | 981 | /** 982 | * @param {Object=} props 983 | * @param {...string|React.ReactComponent|Array.|boolean} children 984 | * @return {React.ReactComponent} 985 | * @protected 986 | */ 987 | React.DOM.line = function(props, children) {}; 988 | 989 | /** 990 | * @param {Object=} props 991 | * @param {...string|React.ReactComponent|Array.|boolean} children 992 | * @return {React.ReactComponent} 993 | * @protected 994 | */ 995 | React.DOM.linearGradient = function(props, children) {}; 996 | 997 | /** 998 | * @param {Object=} props 999 | * @param {...string|React.ReactComponent|Array.|boolean} children 1000 | * @return {React.ReactComponent} 1001 | * @protected 1002 | */ 1003 | React.DOM.path = function(props, children) {}; 1004 | 1005 | /** 1006 | * @param {Object=} props 1007 | * @param {...string|React.ReactComponent|Array.|boolean} children 1008 | * @return {React.ReactComponent} 1009 | * @protected 1010 | */ 1011 | React.DOM.polygon = function(props, children) {}; 1012 | 1013 | /** 1014 | * @param {Object=} props 1015 | * @param {...string|React.ReactComponent|Array.|boolean} children 1016 | * @return {React.ReactComponent} 1017 | * @protected 1018 | */ 1019 | React.DOM.polyline = function(props, children) {}; 1020 | 1021 | /** 1022 | * @param {Object=} props 1023 | * @param {...string|React.ReactComponent|Array.|boolean} children 1024 | * @return {React.ReactComponent} 1025 | * @protected 1026 | */ 1027 | React.DOM.radialGradient = function(props, children) {}; 1028 | 1029 | /** 1030 | * @param {Object=} props 1031 | * @param {...string|React.ReactComponent|Array.|boolean} children 1032 | * @return {React.ReactComponent} 1033 | * @protected 1034 | */ 1035 | React.DOM.rect = function(props, children) {}; 1036 | 1037 | /** 1038 | * @param {Object=} props 1039 | * @param {...string|React.ReactComponent|Array.|boolean} children 1040 | * @return {React.ReactComponent} 1041 | * @protected 1042 | */ 1043 | React.DOM.stop = function(props, children) {}; 1044 | 1045 | /** 1046 | * @param {Object=} props 1047 | * @param {...string|React.ReactComponent|Array.|boolean} children 1048 | * @return {React.ReactComponent} 1049 | * @protected 1050 | */ 1051 | React.DOM.svg = function(props, children) {}; 1052 | 1053 | /** 1054 | * @param {Object=} props 1055 | * @param {...string|React.ReactComponent|Array.|boolean} children 1056 | * @return {React.ReactComponent} 1057 | * @protected 1058 | */ 1059 | React.DOM.text = function(props, children) {}; 1060 | 1061 | /** 1062 | * @typedef {function(boolean, boolean, Object, string, string, string): boolean} React.ChainableTypeChecker 1063 | */ 1064 | React.ChainableTypeChecker; 1065 | 1066 | /** 1067 | * @type {React.ChainableTypeChecker} 1068 | */ 1069 | React.ChainableTypeChecker.weak; 1070 | 1071 | /** 1072 | * @type {React.ChainableTypeChecker} 1073 | */ 1074 | React.ChainableTypeChecker.weak.isRequired; 1075 | 1076 | /** 1077 | * @type {React.ChainableTypeChecker} 1078 | */ 1079 | React.ChainableTypeChecker.isRequired; 1080 | 1081 | /** 1082 | * @type {React.ChainableTypeChecker} 1083 | */ 1084 | React.ChainableTypeChecker.isRequired.weak; 1085 | 1086 | /** 1087 | * @type {Object} 1088 | */ 1089 | React.PropTypes = { 1090 | /** @type {React.ChainableTypeChecker} */ 1091 | any: function () {}, 1092 | /** @type {React.ChainableTypeChecker} */ 1093 | array: function () {}, 1094 | /** @type {React.ChainableTypeChecker} */ 1095 | arrayOf: function () {}, 1096 | /** @type {React.ChainableTypeChecker} */ 1097 | "boolean": function () {}, 1098 | /** @type {React.ChainableTypeChecker} */ 1099 | component: function () {}, 1100 | /** @type {React.ChainableTypeChecker} */ 1101 | func: function () {}, 1102 | /** @type {React.ChainableTypeChecker} */ 1103 | number: function () {}, 1104 | /** @type {React.ChainableTypeChecker} */ 1105 | object: function () {}, 1106 | /** @type {React.ChainableTypeChecker} */ 1107 | string: function () {}, 1108 | /** @type {React.ChainableTypeChecker} */ 1109 | oneOf: function () {}, 1110 | /** @type {React.ChainableTypeChecker} */ 1111 | oneOfType: function () {}, 1112 | /** @type {React.ChainableTypeChecker} */ 1113 | instanceOf: function () {}, 1114 | /** @type {React.ChainableTypeChecker} */ 1115 | renderable: function () {}, 1116 | /** @type {React.ChainableTypeChecker} */ 1117 | shape: function () {} 1118 | }; 1119 | 1120 | /** 1121 | * @type {Object} 1122 | */ 1123 | React.Children; 1124 | 1125 | /** 1126 | * @param {Object} children Children tree container. 1127 | * @param {function(*, int)} mapFunction. 1128 | * @param {*} mapContext Context for mapFunction. 1129 | * @return {Object|undefined} Object containing the ordered map of results. 1130 | */ 1131 | React.Children.map; 1132 | 1133 | /** 1134 | * @param {Object} children Children tree container. 1135 | * @param {function(*, int)} mapFunction. 1136 | * @param {*} mapContext Context for mapFunction. 1137 | */ 1138 | React.Children.forEach; 1139 | 1140 | /** 1141 | * @param {Object} children Children tree container. 1142 | * @return {Object|undefined} 1143 | */ 1144 | React.Children.only; 1145 | -------------------------------------------------------------------------------- /resources/public/css/bootstrap-theme.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v3.0.3 (http://getbootstrap.com) 3 | * Copyright 2013 Twitter, Inc. 4 | * Licensed under http://www.apache.org/licenses/LICENSE-2.0 5 | */ 6 | 7 | .btn-default,.btn-primary,.btn-success,.btn-info,.btn-warning,.btn-danger{text-shadow:0 -1px 0 rgba(0,0,0,0.2);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.15),0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 0 rgba(255,255,255,0.15),0 1px 1px rgba(0,0,0,0.075)}.btn-default:active,.btn-primary:active,.btn-success:active,.btn-info:active,.btn-warning:active,.btn-danger:active,.btn-default.active,.btn-primary.active,.btn-success.active,.btn-info.active,.btn-warning.active,.btn-danger.active{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,0.125);box-shadow:inset 0 3px 5px rgba(0,0,0,0.125)}.btn:active,.btn.active{background-image:none}.btn-default{text-shadow:0 1px 0 #fff;background-image:-webkit-linear-gradient(top,#fff 0,#e0e0e0 100%);background-image:linear-gradient(to bottom,#fff 0,#e0e0e0 100%);background-repeat:repeat-x;border-color:#dbdbdb;border-color:#ccc;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff',endColorstr='#ffe0e0e0',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-default:hover,.btn-default:focus{background-color:#e0e0e0;background-position:0 -15px}.btn-default:active,.btn-default.active{background-color:#e0e0e0;border-color:#dbdbdb}.btn-primary{background-image:-webkit-linear-gradient(top,#428bca 0,#2d6ca2 100%);background-image:linear-gradient(to bottom,#428bca 0,#2d6ca2 100%);background-repeat:repeat-x;border-color:#2b669a;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca',endColorstr='#ff2d6ca2',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-primary:hover,.btn-primary:focus{background-color:#2d6ca2;background-position:0 -15px}.btn-primary:active,.btn-primary.active{background-color:#2d6ca2;border-color:#2b669a}.btn-success{background-image:-webkit-linear-gradient(top,#5cb85c 0,#419641 100%);background-image:linear-gradient(to bottom,#5cb85c 0,#419641 100%);background-repeat:repeat-x;border-color:#3e8f3e;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c',endColorstr='#ff419641',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-success:hover,.btn-success:focus{background-color:#419641;background-position:0 -15px}.btn-success:active,.btn-success.active{background-color:#419641;border-color:#3e8f3e}.btn-warning{background-image:-webkit-linear-gradient(top,#f0ad4e 0,#eb9316 100%);background-image:linear-gradient(to bottom,#f0ad4e 0,#eb9316 100%);background-repeat:repeat-x;border-color:#e38d13;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e',endColorstr='#ffeb9316',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-warning:hover,.btn-warning:focus{background-color:#eb9316;background-position:0 -15px}.btn-warning:active,.btn-warning.active{background-color:#eb9316;border-color:#e38d13}.btn-danger{background-image:-webkit-linear-gradient(top,#d9534f 0,#c12e2a 100%);background-image:linear-gradient(to bottom,#d9534f 0,#c12e2a 100%);background-repeat:repeat-x;border-color:#b92c28;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f',endColorstr='#ffc12e2a',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-danger:hover,.btn-danger:focus{background-color:#c12e2a;background-position:0 -15px}.btn-danger:active,.btn-danger.active{background-color:#c12e2a;border-color:#b92c28}.btn-info{background-image:-webkit-linear-gradient(top,#5bc0de 0,#2aabd2 100%);background-image:linear-gradient(to bottom,#5bc0de 0,#2aabd2 100%);background-repeat:repeat-x;border-color:#28a4c9;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de',endColorstr='#ff2aabd2',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-info:hover,.btn-info:focus{background-color:#2aabd2;background-position:0 -15px}.btn-info:active,.btn-info.active{background-color:#2aabd2;border-color:#28a4c9}.thumbnail,.img-thumbnail{-webkit-box-shadow:0 1px 2px rgba(0,0,0,0.075);box-shadow:0 1px 2px rgba(0,0,0,0.075)}.dropdown-menu>li>a:hover,.dropdown-menu>li>a:focus{background-color:#e8e8e8;background-image:-webkit-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:linear-gradient(to bottom,#f5f5f5 0,#e8e8e8 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5',endColorstr='#ffe8e8e8',GradientType=0)}.dropdown-menu>.active>a,.dropdown-menu>.active>a:hover,.dropdown-menu>.active>a:focus{background-color:#357ebd;background-image:-webkit-linear-gradient(top,#428bca 0,#357ebd 100%);background-image:linear-gradient(to bottom,#428bca 0,#357ebd 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca',endColorstr='#ff357ebd',GradientType=0)}.navbar-default{background-image:-webkit-linear-gradient(top,#fff 0,#f8f8f8 100%);background-image:linear-gradient(to bottom,#fff 0,#f8f8f8 100%);background-repeat:repeat-x;border-radius:4px;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff',endColorstr='#fff8f8f8',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.15),0 1px 5px rgba(0,0,0,0.075);box-shadow:inset 0 1px 0 rgba(255,255,255,0.15),0 1px 5px rgba(0,0,0,0.075)}.navbar-default .navbar-nav>.active>a{background-image:-webkit-linear-gradient(top,#ebebeb 0,#f3f3f3 100%);background-image:linear-gradient(to bottom,#ebebeb 0,#f3f3f3 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb',endColorstr='#fff3f3f3',GradientType=0);-webkit-box-shadow:inset 0 3px 9px rgba(0,0,0,0.075);box-shadow:inset 0 3px 9px rgba(0,0,0,0.075)}.navbar-brand,.navbar-nav>li>a{text-shadow:0 1px 0 rgba(255,255,255,0.25)}.navbar-inverse{background-image:-webkit-linear-gradient(top,#3c3c3c 0,#222 100%);background-image:linear-gradient(to bottom,#3c3c3c 0,#222 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3c3c3c',endColorstr='#ff222222',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.navbar-inverse .navbar-nav>.active>a{background-image:-webkit-linear-gradient(top,#222 0,#282828 100%);background-image:linear-gradient(to bottom,#222 0,#282828 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff222222',endColorstr='#ff282828',GradientType=0);-webkit-box-shadow:inset 0 3px 9px rgba(0,0,0,0.25);box-shadow:inset 0 3px 9px rgba(0,0,0,0.25)}.navbar-inverse .navbar-brand,.navbar-inverse .navbar-nav>li>a{text-shadow:0 -1px 0 rgba(0,0,0,0.25)}.navbar-static-top,.navbar-fixed-top,.navbar-fixed-bottom{border-radius:0}.alert{text-shadow:0 1px 0 rgba(255,255,255,0.2);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.25),0 1px 2px rgba(0,0,0,0.05);box-shadow:inset 0 1px 0 rgba(255,255,255,0.25),0 1px 2px rgba(0,0,0,0.05)}.alert-success{background-image:-webkit-linear-gradient(top,#dff0d8 0,#c8e5bc 100%);background-image:linear-gradient(to bottom,#dff0d8 0,#c8e5bc 100%);background-repeat:repeat-x;border-color:#b2dba1;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8',endColorstr='#ffc8e5bc',GradientType=0)}.alert-info{background-image:-webkit-linear-gradient(top,#d9edf7 0,#b9def0 100%);background-image:linear-gradient(to bottom,#d9edf7 0,#b9def0 100%);background-repeat:repeat-x;border-color:#9acfea;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7',endColorstr='#ffb9def0',GradientType=0)}.alert-warning{background-image:-webkit-linear-gradient(top,#fcf8e3 0,#f8efc0 100%);background-image:linear-gradient(to bottom,#fcf8e3 0,#f8efc0 100%);background-repeat:repeat-x;border-color:#f5e79e;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3',endColorstr='#fff8efc0',GradientType=0)}.alert-danger{background-image:-webkit-linear-gradient(top,#f2dede 0,#e7c3c3 100%);background-image:linear-gradient(to bottom,#f2dede 0,#e7c3c3 100%);background-repeat:repeat-x;border-color:#dca7a7;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede',endColorstr='#ffe7c3c3',GradientType=0)}.progress{background-image:-webkit-linear-gradient(top,#ebebeb 0,#f5f5f5 100%);background-image:linear-gradient(to bottom,#ebebeb 0,#f5f5f5 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb',endColorstr='#fff5f5f5',GradientType=0)}.progress-bar{background-image:-webkit-linear-gradient(top,#428bca 0,#3071a9 100%);background-image:linear-gradient(to bottom,#428bca 0,#3071a9 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca',endColorstr='#ff3071a9',GradientType=0)}.progress-bar-success{background-image:-webkit-linear-gradient(top,#5cb85c 0,#449d44 100%);background-image:linear-gradient(to bottom,#5cb85c 0,#449d44 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c',endColorstr='#ff449d44',GradientType=0)}.progress-bar-info{background-image:-webkit-linear-gradient(top,#5bc0de 0,#31b0d5 100%);background-image:linear-gradient(to bottom,#5bc0de 0,#31b0d5 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de',endColorstr='#ff31b0d5',GradientType=0)}.progress-bar-warning{background-image:-webkit-linear-gradient(top,#f0ad4e 0,#ec971f 100%);background-image:linear-gradient(to bottom,#f0ad4e 0,#ec971f 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e',endColorstr='#ffec971f',GradientType=0)}.progress-bar-danger{background-image:-webkit-linear-gradient(top,#d9534f 0,#c9302c 100%);background-image:linear-gradient(to bottom,#d9534f 0,#c9302c 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f',endColorstr='#ffc9302c',GradientType=0)}.list-group{border-radius:4px;-webkit-box-shadow:0 1px 2px rgba(0,0,0,0.075);box-shadow:0 1px 2px rgba(0,0,0,0.075)}.list-group-item.active,.list-group-item.active:hover,.list-group-item.active:focus{text-shadow:0 -1px 0 #3071a9;background-image:-webkit-linear-gradient(top,#428bca 0,#3278b3 100%);background-image:linear-gradient(to bottom,#428bca 0,#3278b3 100%);background-repeat:repeat-x;border-color:#3278b3;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca',endColorstr='#ff3278b3',GradientType=0)}.panel{-webkit-box-shadow:0 1px 2px rgba(0,0,0,0.05);box-shadow:0 1px 2px rgba(0,0,0,0.05)}.panel-default>.panel-heading{background-image:-webkit-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:linear-gradient(to bottom,#f5f5f5 0,#e8e8e8 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5',endColorstr='#ffe8e8e8',GradientType=0)}.panel-primary>.panel-heading{background-image:-webkit-linear-gradient(top,#428bca 0,#357ebd 100%);background-image:linear-gradient(to bottom,#428bca 0,#357ebd 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca',endColorstr='#ff357ebd',GradientType=0)}.panel-success>.panel-heading{background-image:-webkit-linear-gradient(top,#dff0d8 0,#d0e9c6 100%);background-image:linear-gradient(to bottom,#dff0d8 0,#d0e9c6 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8',endColorstr='#ffd0e9c6',GradientType=0)}.panel-info>.panel-heading{background-image:-webkit-linear-gradient(top,#d9edf7 0,#c4e3f3 100%);background-image:linear-gradient(to bottom,#d9edf7 0,#c4e3f3 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7',endColorstr='#ffc4e3f3',GradientType=0)}.panel-warning>.panel-heading{background-image:-webkit-linear-gradient(top,#fcf8e3 0,#faf2cc 100%);background-image:linear-gradient(to bottom,#fcf8e3 0,#faf2cc 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3',endColorstr='#fffaf2cc',GradientType=0)}.panel-danger>.panel-heading{background-image:-webkit-linear-gradient(top,#f2dede 0,#ebcccc 100%);background-image:linear-gradient(to bottom,#f2dede 0,#ebcccc 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede',endColorstr='#ffebcccc',GradientType=0)}.well{background-image:-webkit-linear-gradient(top,#e8e8e8 0,#f5f5f5 100%);background-image:linear-gradient(to bottom,#e8e8e8 0,#f5f5f5 100%);background-repeat:repeat-x;border-color:#dcdcdc;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe8e8e8',endColorstr='#fff5f5f5',GradientType=0);-webkit-box-shadow:inset 0 1px 3px rgba(0,0,0,0.05),0 1px 0 rgba(255,255,255,0.1);box-shadow:inset 0 1px 3px rgba(0,0,0,0.05),0 1px 0 rgba(255,255,255,0.1)} -------------------------------------------------------------------------------- /resources/public/css/style.css: -------------------------------------------------------------------------------- 1 | @import "bootstrap.min.css"; 2 | @import "bootstrap-theme.min.css"; 3 | 4 | body { 5 | font-family: 'Open Sans', sans-serif; 6 | overflow-x: hidden; 7 | } 8 | 9 | .name { 10 | font-style: italic; 11 | font-weight: bold; 12 | text-transform: uppercase; 13 | float: left; 14 | } 15 | 16 | .links { 17 | text-transform: uppercase; 18 | font-style: italic; 19 | float: right; 20 | } 21 | 22 | .listing { 23 | margin: 20px 0px; 24 | } 25 | 26 | .listing .list-item { 27 | background-color: #efefef; 28 | border: 1px solid white; 29 | padding: 10px; 30 | box-sizing: border-box; 31 | } 32 | 33 | .listing .title { 34 | margin-bottom: 5px; 35 | } 36 | 37 | .file { 38 | color: #666; 39 | font-weight: normal; 40 | } 41 | .dir { 42 | color: #444; 43 | font-weight: bold; 44 | } 45 | .listing .list-item-name { 46 | font-weight: bold; 47 | word-wrap: break-word; 48 | } 49 | 50 | .listing .subitem { 51 | font-weight: normal; 52 | color: #999; 53 | } 54 | 55 | .listing .list-item-size { 56 | text-align: right; 57 | padding-right: 20px; 58 | } 59 | .listing .list-item-modified { 60 | padding-right: 20px; 61 | text-align: right; 62 | } 63 | .listing .list-item-tag { 64 | font-weight: bold; 65 | font-style: italic; 66 | } 67 | .listing .list-item-tag-button { 68 | } 69 | 70 | .current-path { 71 | font-family: 'Open Sans', sans-serif; 72 | color: #999; 73 | } 74 | 75 | .floating-overlay { 76 | position: fixed; 77 | top: 20px; 78 | left: 15%; 79 | right: 15%; 80 | z-index: 10000; 81 | 82 | word-wrap: break-word; 83 | 84 | padding: 10px; 85 | 86 | border: 1px solid black; 87 | background-color: #333; 88 | color: white; 89 | box-shadow: 1px 1px 1px #888888; 90 | font-size: 20px; 91 | } 92 | 93 | .floating-overlay .download-item { 94 | padding: 5px; 95 | } 96 | 97 | .floating-overlay .progress-bar { 98 | height: 2px; 99 | background-color: #555; 100 | opacity: 0.5; 101 | } 102 | 103 | .floating-overlay .filename { 104 | font-weight: bold; 105 | } 106 | 107 | .floating-overlay .sub { 108 | text-align:right; 109 | } 110 | 111 | .floating-overlay .waiting { 112 | font-style: italic; 113 | } 114 | 115 | .floating-overlay .controls { 116 | margin-top: 30px; 117 | } 118 | 119 | @media (max-width: 980px) { 120 | .floating-overlay { 121 | font-size: 15px; 122 | } 123 | } 124 | 125 | @media (max-width: 780px) { 126 | .floating-overlay .sub { 127 | text-align: left; 128 | } 129 | } 130 | 131 | @media (max-width: 568px) { 132 | .floating-overlay { 133 | top: 0px; 134 | left: 0px; 135 | right: 0px; 136 | border: none; 137 | } 138 | } 139 | 140 | .current-path a { 141 | padding: 0px 5px; 142 | } 143 | a.target-link:hover { 144 | text-decoration: none; 145 | cursor: pointer; 146 | } 147 | 148 | .no-files { 149 | position: absolute; 150 | left: 0px; 151 | right: 0px; 152 | top: 200px; 153 | text-align: center; 154 | color: #999; 155 | font-style: italic; 156 | font-size: 25px; 157 | } 158 | 159 | .dim { 160 | position: fixed; 161 | left: 0px; 162 | top: 0px; 163 | width: 100%; 164 | height: 100%; 165 | z-index: 1000 !important; 166 | background-color: white; 167 | opacity: 0.5; 168 | } 169 | 170 | #error .message { 171 | position: absolute; 172 | left: 0px; 173 | width: 100%; 174 | top: 200px; 175 | height: 50px; 176 | text-align: center; 177 | color: #aa0000; 178 | font-weight: bold; 179 | font-size: 20px; 180 | font-style: italic; 181 | } 182 | 183 | .dim .loader { 184 | position: absolute; 185 | left: 0px; 186 | width: 100%; 187 | top: 200px; 188 | height: 50px; 189 | background-repeat: no-repeat; 190 | background-position: center center; 191 | background-image: url(data:image/gif;base64,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); 192 | } 193 | 194 | .loader { 195 | width: 100%; 196 | height: 60px; 197 | background-repeat: no-repeat; 198 | background-position: center center; 199 | background-image: url(data:image/gif;base64,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); 200 | } 201 | 202 | 203 | .stats { 204 | margin-top: 30px; 205 | text-transform: uppercase; 206 | font-weight: bold; 207 | float: right; 208 | } 209 | 210 | .stats .downloads { 211 | } 212 | 213 | .stats .tags { 214 | margin-right: 20px; 215 | } 216 | 217 | .nodownloads { 218 | width: 100%; 219 | text-align: center; 220 | color: #999; 221 | font-style: italic; 222 | } 223 | 224 | .current-downloads .section-title { 225 | font-size: 25px; 226 | font-weight: bold; 227 | font-style: italic; 228 | text-transform: uppercase; 229 | color: #007883; 230 | } 231 | .current-downloads .download-item { 232 | margin-bottom: 5px; 233 | padding-left: 10px; 234 | padding-right: 10px; 235 | } 236 | 237 | .current-downloads .download-item .title { 238 | font-size: 18px; 239 | font-weight: bold; 240 | overflow: hidden; 241 | } 242 | 243 | .current-downloads .download-item .desc { 244 | font-size: 12px; 245 | color: #999; 246 | } 247 | 248 | .current-downloads .download-item .status { 249 | text-transform: uppercase; 250 | font-weight: bold; 251 | color: #999; 252 | } 253 | 254 | .current-downloads .download-item .status div { 255 | text-align: right; 256 | } 257 | 258 | .tag-button-container { 259 | text-align: right; 260 | } 261 | 262 | .tag-button-container .tag-item-action { 263 | width: 100%; 264 | color: #999; 265 | } 266 | 267 | .all-tags a { 268 | font-size: 20px; 269 | font-style: italic; 270 | width: 100%; 271 | display: block; 272 | line-height: 30px; 273 | text-align: center; 274 | padding: 10px; 275 | color: black; 276 | cursor: pointer; 277 | } 278 | 279 | .all-tags a:hover { 280 | text-decoration: none; 281 | color: gray; 282 | } 283 | 284 | @media (max-width: 320px) { 285 | .stats { 286 | float: left; 287 | margin-top: 0px; 288 | } 289 | 290 | } 291 | @media (max-width: 480px) { 292 | .listing { 293 | font-size: 12px; 294 | } 295 | 296 | .listing .list-item-modified { 297 | text-align: left; 298 | } 299 | .listing .list-item-size { 300 | text-align: left; 301 | } 302 | 303 | .current-path { 304 | font-size: 10px; 305 | word-wrap: break-word; 306 | } 307 | } 308 | 309 | @media (max-width: 768px) { 310 | .listing { 311 | font-size: 16px; 312 | } 313 | 314 | .current-path { 315 | font-size: 16px; 316 | font-weight: bold; 317 | } 318 | } 319 | 320 | @media (max-width: 1024px) { 321 | .current-path { 322 | font-size: 16px; 323 | font-weight: bold; 324 | } 325 | } 326 | 327 | @media (min-width: 1024px) { 328 | .listing { 329 | font-size: 18px; 330 | } 331 | .current-path { 332 | font-size: 18px; 333 | font-weight: bold; 334 | } 335 | } 336 | 337 | .thin-progress { 338 | height: 2px; 339 | width: 100%; 340 | margin-top: 5px; 341 | } 342 | 343 | .thin-progress-bar { 344 | height: 100%; 345 | background-color: #007883; 346 | } 347 | 348 | .download-monitor { 349 | margin-top: 20px; 350 | } 351 | 352 | .no-tags { 353 | font-style: italic; 354 | color: #999; 355 | text-align: center; 356 | } 357 | 358 | .tag-info { 359 | margin-right: 10px; 360 | } 361 | .dl-info { 362 | font-weight: normal; 363 | font-style: normal; 364 | } 365 | 366 | .activity-monitor { 367 | text-align: right; 368 | text-transform: uppercase; 369 | font-weight: 700; 370 | font-style: italic; 371 | color: #999; 372 | padding-top: 5px; 373 | } 374 | 375 | .actions-menu { 376 | text-transform: uppercase; 377 | font-weight:bold; 378 | } 379 | -------------------------------------------------------------------------------- /resources/public/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verma/dakait/68669e0aea17698806fd62ec72508b19307de594/resources/public/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /resources/public/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verma/dakait/68669e0aea17698806fd62ec72508b19307de594/resources/public/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /resources/public/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verma/dakait/68669e0aea17698806fd62ec72508b19307de594/resources/public/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /resources/templates/index.debug.mustache: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | {{server-name}} - Dakait File Listing 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 26 | 27 | 28 | 29 | 30 |
31 |
32 |
33 | 34 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /resources/templates/index.mustache: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | {{server-name}} - Dakait File Listing 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 24 | 25 | 26 | 27 | 28 |
29 |
30 |
31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /resources/templates/tags.mustache: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | {{server-name}} - Tags 13 | 14 | 15 | 16 | 17 | 85 | 86 | 87 | 88 | 89 | 90 | 94 | 95 | 96 | 97 | 98 |
99 |
100 |

{{server-name}} - Tags

101 |

Files

102 |
103 |
104 | 105 | 106 | 107 |
108 |
109 |
110 | 111 | 112 |
113 |
114 | 115 | 116 |
117 | 118 |
119 |
120 |
121 | 122 | 123 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | -------------------------------------------------------------------------------- /src-cljs/dakait/components.cljs: -------------------------------------------------------------------------------- 1 | (ns dakait.components 2 | (:use [dakait.util :only [format-file-size format-date duration-since]] 3 | [clojure.string :only [join split]] 4 | [jayq.core :only [$ html append css text ajax on bind hide show attr add-class remove-class]] 5 | [dakait.net :only [http-post]] 6 | [jayq.util :only [log]]) 7 | (:require [cljs.core.async :as async :refer [chan >! put!]] 8 | [om.core :as om :include-macros true] 9 | [om.dom :as dom :include-macros true] 10 | [cljs.core.match]) 11 | (:require-macros [cljs.core.async.macros :refer [go]] 12 | [cljs.core.match.macros :refer [match]])) 13 | 14 | (defn current-path 15 | "Manages the current path display" 16 | [path owner {:keys [path-chan] :as opts}] 17 | (reify 18 | om/IRender 19 | (render [this] 20 | (let [parts (split path #"/") 21 | paths (->> parts 22 | (reduce (fn [acc p] 23 | (cons (cons p (first acc)) acc)) (list)) 24 | (map #(->> % reverse (join "/"))) 25 | reverse) 26 | handler (fn [p] (fn [] (go (>! path-chan p))))] 27 | (apply dom/h3 #js {:className "current-path"} 28 | (->> (map (fn [p path] (dom/a #js {:href "#" :onClick (handler path)} p)) parts paths) 29 | (interpose " / "))))))) 30 | 31 | (defn listing 32 | "Manages and shows the files listing" 33 | [{:keys [listing current-path]} owner {:keys [path-chan modal-chan] :as opts}] 34 | (reify 35 | om/IRender 36 | (render [this] 37 | (apply dom/div #js {:className "listing"} 38 | (map (fn [l] 39 | (let [push-handler (fn [name] 40 | (fn [] 41 | (go (>! path-chan (str current-path "/" name))))) 42 | tag-handler (fn [name] 43 | (fn [] 44 | (log "Requesting tag for " name) 45 | (go (>! modal-chan (str current-path "/" name))))) 46 | item-class (str "list-item " (:type l)) 47 | item-modified (format-date (:modified l)) 48 | item-size (if (= (:type l) "dir") "" (format-file-size (:size l))) 49 | tag-item (let [tag (:tag l) 50 | dl (:download l) 51 | color (if tag (:color tag) "#999") 52 | dlinfo (match dl 53 | {:available st} (str (:percent-complete st) ", " (:rate st) ", eta: " (:eta st)) 54 | {:waiting _} "Waiting..." 55 | :else "")] 56 | (dom/div #js {:className "col-sm-6 list-item-tag" 57 | :style #js {:color color}} 58 | (when tag (dom/span #js {:className "tag-info"} (:name tag))) 59 | (when dl (dom/span #js {:className "dl-info"} dlinfo))))] 60 | (dom/div #js {:className item-class 61 | :key (:name l)} 62 | (dom/div #js {:className "row"} 63 | (dom/div #js {:className "col-sm-10"} 64 | (dom/div #js {:className "row"} 65 | (dom/div #js {:className "col-sm-10 list-item-name"} 66 | (if (= (:type l) "dir") 67 | (dom/a #js {:className "target-link" :href "#" :onClick (push-handler (:name l))} (:name l)) 68 | (dom/span nil (:name l)))) 69 | (dom/div #js {:className "col-sm-2 list-item-size"} item-size)) 70 | (dom/div #js {:className "row subitem"} 71 | tag-item 72 | (dom/div #js {:className "col-sm-6 list-item-modified"} item-modified))) 73 | (dom/div #js {:className "col-sm-2 tag-button-container"} 74 | (dom/button #js {:className "btn btn-default btn-lg tag-item-action" 75 | :type "button" 76 | :disabled (:recent l) 77 | :onClick (tag-handler (:name l)) } "Tag"))) 78 | (when-let [pc (get-in l [:download :available :percent-complete])] 79 | (dom/div #js {:className "thin-progress"} 80 | (dom/div #js {:className "thin-progress-bar" 81 | :style #js {:width pc}})))))) 82 | listing))))) 83 | 84 | 85 | (defn- file-name [p] 86 | (-> p 87 | (clojure.string/split #"/") 88 | last)) 89 | 90 | (defn overlay-download-summary-item 91 | "Shows a single overlay download" 92 | [download owner] 93 | (reify 94 | om/IRender 95 | (render [_] 96 | (dom/div nil 97 | (apply dom/div 98 | #js {:className "row download-item"} 99 | (dom/div 100 | #js {:className "col-sm-6 col-xs-12 filename"} 101 | (file-name (:from download))) 102 | (if-let [ds (:download-status download)] 103 | [(dom/div #js {:className "col-sm-2 col-xs-4 sub pc"} (:percent-complete ds)) 104 | (dom/div #js {:className "col-sm-2 col-xs-4 sub rate"} (:rate ds)) 105 | (dom/div #js {:className "col-sm-2 col-xs-4 sub ds"} (:eta ds))] 106 | [(dom/div #js {:className "col-sm-6 sub waiting"} "Waiting...")])) 107 | (dom/div (clj->js {:className "progress-bar" 108 | :style (clj->js {:width (if (:download-status download) 109 | (get-in download [:download-status :percent-complete]) 110 | "0%")})}) " "))))) 111 | 112 | 113 | 114 | (defn overlay-download-summary 115 | "A floating overlay widget which shows the current status of all downloads" 116 | [downloads owner {:keys [hide-chan] :as opts}] 117 | (reify 118 | om/IRender 119 | (render [_] 120 | (let [dls (:active downloads)] 121 | (dom/div 122 | #js {:className "floating-overlay"} 123 | (if (-> dls count zero?) 124 | (dom/div nil "There are no active downloads at this time") 125 | (dom/div nil 126 | (apply dom/div #js {:className "container-fluid" 127 | :style #js {:position "relative"}} 128 | (om/build-all overlay-download-summary-item dls)))) 129 | (dom/div #js {:className "container-fluid controls"} 130 | (dom/div #js {:className "col-sm-4 col-sm-offset-4"} 131 | (dom/button #js {:type "button" 132 | :className "btn btn-primary btn-sm btn-block" 133 | :onClick #(put! hide-chan 0)} 134 | "Close")))))))) 135 | 136 | (defn download-activity-monitor 137 | "A little widget that shows download activity" 138 | [downloads owner] 139 | (reify 140 | om/IInitState 141 | (init-state [_] 142 | {:show-summary false 143 | :hide-chan (chan)}) 144 | 145 | om/IWillMount 146 | (will-mount [_] 147 | (let [c (om/get-state owner :hide-chan)] 148 | (go (loop [v (bw (fn [dl] 163 | (if-let [ds (:download-status dl)] (as-kb (:rate ds)) 0)) 164 | tb (reduce #(+ %1 (dl->bw %2)) 0 (:active downloads)) 165 | tb-str (if (< tb 1000) 166 | (str (.toFixed tb 1) "KB/s") 167 | (str (.toFixed (/ tb 1000) 1) "MB/s"))] 168 | 169 | (dom/div #js {:className "download-monitor pull-right"} 170 | (when show-summary 171 | (om/build overlay-download-summary downloads {:opts {:hide-chan hide-chan}})) 172 | (when-not (zero? (+ dls pen)) 173 | (dom/div #js {:className "activity-monitor"} 174 | (dom/a #js {:href "#" 175 | :onClick #(om/update-state! owner :show-summary not)} 176 | (str "Active: " dls " Pending: " pen)) 177 | (dom/div nil tb-str)))))))) 178 | 179 | (defn sort-order 180 | "Manages sort order and indicates changes over a channel" 181 | [[key asc] owner {:keys [sort-order-chan] :as opts}] 182 | (reify 183 | om/IRender 184 | (render [_] 185 | (let [gen-handler (fn [k] 186 | (fn [] 187 | (log "clicked") 188 | (go (>! sort-order-chan [k (if (= k key) (not asc) asc)])))) 189 | grp-button (fn [k title] 190 | (dom/a #js {:className "btn btn-default" :role "button" :onClick (gen-handler k)} 191 | title 192 | " " 193 | (when (= k key) 194 | (dom/span #js {:className (str "glyphicon glyphicon-chevron-" (if asc "up" "down"))} ""))))] 195 | (log key asc) 196 | (dom/div #js {:className "btn-group btn-group-justified"} 197 | (grp-button :name "Name") 198 | (grp-button :size "Size") 199 | (grp-button :modified "Modified")))))) 200 | 201 | (defn- add-tag-form 202 | "A horizontal add tag form" 203 | [_ owner {:keys [add-cb] :as opts}] 204 | (reify 205 | om/IInitState 206 | (init-state [_] 207 | {:ready false}) 208 | om/IDidMount 209 | (did-mount [_] 210 | (.focus (om/get-node owner "name"))) 211 | 212 | om/IRenderState 213 | (render-state [_ state] 214 | (let [values (fn [] 215 | [(.-value (om/get-node owner "name")) 216 | (.-value (om/get-node owner "target"))]) 217 | text-changed (fn [] 218 | (om/set-state! owner :ready 219 | (every? #(> (count %) 0) (values)))) 220 | trigger-add (fn [] 221 | (let [[name target] (values)] 222 | (set! (.-value (om/get-node owner "name")) "") 223 | (set! (.-value (om/get-node owner "target")) "") 224 | (.focus (om/get-node owner "name")) 225 | (add-cb name target))) 226 | key-up (fn [e] 227 | (when (and (= (.-keyCode e) 13) 228 | (om/get-state owner :ready)) 229 | (trigger-add)))] 230 | (dom/div #js {:className "panel panel-default" 231 | :style #js {:marginTop "10px"}} 232 | (dom/div #js {:className "panel-body"} 233 | (dom/input #js {:type "text" 234 | :className "form-control" 235 | :placeholder "Name" 236 | :ref "name" 237 | :onChange text-changed 238 | :onKeyUp key-up 239 | :style #js {:marginBottom "5px"}}) 240 | (dom/input #js {:type "text" 241 | :className "form-control" 242 | :placeholder "Target Path" 243 | :onChange text-changed 244 | :ref "target" 245 | :onKeyUp key-up 246 | :style #js {:marginBottom "5px"}}) 247 | (dom/div #js {:className "row"} 248 | (dom/div #js {:className "col-md-12"} 249 | (dom/button #js {:className "btn btn-success btn-block" 250 | :disabled (not (:ready state)) 251 | :onClick trigger-add 252 | :style #js {:marginTop "5px"}} "Add"))))))))) 253 | 254 | (defn tags-modal 255 | "The tags modal component, allows users to pick a tag to associate with 256 | a file, also allows them to create a new tag if need be, needs a few channels to make things 257 | work, a channel to show popup, a channel that recieves the response, and a channel that is notified 258 | about any new tags being added" 259 | [tags owner {:keys [modal-chan apply-chan add-chan remove-chan] :as opts}] 260 | (reify 261 | om/IInitState 262 | (init-state [_] 263 | {:target nil 264 | :adding false}) 265 | 266 | om/IDidMount 267 | (did-mount [this] 268 | (go (loop [] 269 | (log "in loop") 270 | (let [file (! apply-chan [(:target state) name])) 281 | (close-dialog))) 282 | remove-handler (fn [name] (fn [] 283 | (go (>! remove-chan name))))] 284 | (dom/div #js {:className "modal fade" :role "dialog"} 285 | (dom/div #js {:className "modal-dialog modal-sm"} 286 | (dom/div #js {:className "modal-content"} 287 | (dom/div #js {:className "modal-header"} 288 | (dom/button #js {:type "button" :className "close" :onClick close-dialog} "x") 289 | (dom/h4 #js {:className "modal-title"} "Choose a Tag")) 290 | (dom/div #js {:className "modal-body"} 291 | (when (zero? (count tags)) 292 | (dom/div #js {:className "no-tags"} "There don't seem to be any tags defined, add one!")) 293 | (apply dom/table #js {:className "all-tags" 294 | :style #js {:width "100%"}} 295 | (map #(dom/tr nil 296 | (dom/td nil 297 | (dom/a #js {:className "tag-item" 298 | :onClick (if (:editing state) (fn[]) (make-handler (:name %))) 299 | :style #js {:backgroundColor (:color %)}} 300 | (:name %))) 301 | (when (:editing state) 302 | (dom/td #js {:style #js {:width "50px"}} 303 | (dom/button #js {:className "btn btn-sm btn-danger col-md-1" 304 | :onClick (remove-handler (:name %)) 305 | :style #js {:width "100%" :height "50px"}} 306 | (dom/span #js {:className "glyphicon glyphicon-remove"}))))) 307 | tags)) 308 | (when (:editing state) 309 | (om/build add-tag-form 310 | tags ;; we don't really need to pass anything in here, triggers bug 311 | {:opts {:add-cb (fn [name path] 312 | (go (>! add-chan [name path])))}}))) 313 | (dom/div #js {:className "modal-footer"} 314 | (dom/button #js {:className (str "btn" " " (if (:editing state) "btn-danger" "btn-success")) 315 | :onClick (fn [] (om/update-state! owner :editing not))} 316 | (dom/span #js {:className "glyphicon glyphicon-edit"}) 317 | " " 318 | (if (:editing state) "Done" "Edit")) 319 | (dom/button #js {:className "btn btn-defalt btn-info" 320 | :type "button" 321 | :onClick close-dialog } "Close"))))))))) 322 | 323 | (defn content-pusher-modal 324 | "This modal accepts a URL to a resource, downloads the resource and pushes it back to the server to 325 | a specified location configured on the server side" 326 | [_ owner {:keys [pusher-modal-chan] :as opts}] 327 | (reify 328 | om/IInitState 329 | (init-state [_] 330 | {:can-accept false 331 | :adding false}) 332 | 333 | om/IDidMount 334 | (did-mount [this] 335 | (let [$modal ($ (om/get-node owner)) 336 | url (om/get-node owner "url")] 337 | (.modal $modal #js {:keyboard false 338 | :show false 339 | :backdrop "static"}) 340 | (.on $modal "shown.bs.modal" (fn [] 341 | (.focus url))) 342 | (go (loop [] 343 | ( (count v) 0))) 358 | ;; Handler to handle an upload request 359 | ;; 360 | handle-upload (fn [] 361 | (let [v (.-value (om/get-node owner "url"))] 362 | (log "Pushing upload " v) 363 | (om/set-state! owner :processing true) 364 | (http-post "/a/push" {:url v} 365 | #(close-dialog) 366 | #(do 367 | (om/set-state! owner :error true) 368 | (om/set-state! owner :processing false)))))] 369 | (dom/div #js {:className "modal fade" :role "dialog"} 370 | (dom/div #js {:className "modal-dialog modal-sm"} 371 | (dom/div #js {:className "modal-content"} 372 | (dom/div #js {:className "modal-body"} 373 | (dom/input #js {:className "form-control input-lg" 374 | :style #js {:margin "20px 0px"} 375 | :ref "url" 376 | :onChange change-handler 377 | :disabled (:processing state) 378 | :placeholder "Resource URL"}) 379 | (when (:processing state) 380 | (dom/div #js {:className "loader"})) 381 | (when (:error state) 382 | (dom/div #js {:className "alert alert-danger"} 383 | "There seems to be a problem pushing your upload, check logs?")) 384 | (dom/button #js {:className "btn btn-sm btn-success btn-block" 385 | :disabled (or (not (:can-accept state)) (:processing state)) 386 | :onClick handle-upload 387 | :style #js {:marginBottom "5px"}} "OK") 388 | (dom/button #js {:className "btn btn-sm btn-info btn-block" 389 | :disabled (:processing state) 390 | :onClick close-dialog} "Close"))))))))) 391 | -------------------------------------------------------------------------------- /src-cljs/dakait/downloads.cljs: -------------------------------------------------------------------------------- 1 | (ns dakait.downloads 2 | (:require [cljs.core.async :refer [>!]]) 3 | (:require-macros [cljs.core.async.macros :refer [go]])) 4 | 5 | (defn start-listening-for-downloads 6 | "Starts the downloads notifications listener and notifies over the given chan" 7 | [post-chan] 8 | (let [uri (str "ws://" (.-host (.-location js/window)) "/ws/downloads") 9 | ws (js/WebSocket. uri)] 10 | (doto ws 11 | (aset "onmessage" 12 | (fn [data] 13 | (let [json-obj (.parse js/JSON (.-data data)) 14 | edn-obj (js->clj json-obj :keywordize-keys true)] 15 | (go (>! post-chan edn-obj)))))))) 16 | -------------------------------------------------------------------------------- /src-cljs/dakait/net.cljs: -------------------------------------------------------------------------------- 1 | ;; net utilities 2 | ;; 3 | (ns dakait.net) 4 | 5 | (defn get-json 6 | "Sends a get request to the server and gets back with data already EDNed" 7 | ([path response-cb error-cb] 8 | (get-json path {} response-cb error-cb)) 9 | ([path params response-cb error-cb] 10 | (let [r (.get js/jQuery path (clj->js params))] 11 | (doto r 12 | (.done (fn [data] 13 | (response-cb (js->clj data :keywordize-keys true)))) 14 | (.fail (fn [e] 15 | (error-cb (js->clj (.-responseJSON e) :keywordize-keys true)))))))) 16 | 17 | (defn http-post 18 | "Post an HTTP request" 19 | [path params scb ecb] 20 | (let [r (.ajax js/jQuery 21 | (clj->js {:url path 22 | :type "POST" 23 | :data params}))] 24 | (doto r 25 | (.success scb) 26 | (.fail ecb)))) 27 | 28 | (defn http-delete 29 | "Post an HTTP delete request" 30 | [url scb ecb] 31 | (let [r (.ajax js/jQuery 32 | (clj->js {:url url 33 | :type "DELETE"}))] 34 | (doto r 35 | (.success scb) 36 | (.fail ecb)))) 37 | -------------------------------------------------------------------------------- /src-cljs/dakait/start.cljs: -------------------------------------------------------------------------------- 1 | (ns dakait.index 2 | (:use [dakait.components :only [current-path listing sort-order 3 | download-activity-monitor tags-modal content-pusher-modal]] 4 | [dakait.util :only [duration-since]] 5 | [dakait.net :only [get-json http-delete http-post]] 6 | [dakait.downloads :only [start-listening-for-downloads]] 7 | [clojure.string :only [join split]] 8 | [jayq.core :only [$ html append css text ajax on bind hide show attr add-class remove-class]] 9 | [jayq.util :only [log]]) 10 | (:require [cljs.core.async :refer [chan clj (.-serverVars js/window) :keywordize-keys true)) 17 | 18 | (def app-state (atom {:name "" 19 | :downloads {:active [] 20 | :pending []} 21 | :current-path "." 22 | :listing [] 23 | :tags [] })) 24 | 25 | ;; Sort map, each function takes one argument which indicates whether we intend 26 | ;; to do an ascending or a descending sort 27 | ;; 28 | (def sort-funcs 29 | {:name (fn [items asc] 30 | (sort-by 31 | #(str (:type %) (:name %)) 32 | (if (true? asc) compare (comp - compare)) 33 | items)) 34 | 35 | :size (fn [items asc] 36 | (sort-by 37 | (if (true? asc) 38 | :size 39 | #(- (:size %))) 40 | items)) 41 | 42 | :modified (fn [items asc] 43 | (sort-by 44 | (if (true? asc) 45 | :modified 46 | #(- (:modified %))) 47 | items)) 48 | }) 49 | 50 | (defn get-config 51 | "Gets configuration from the server, what it include depends on what 52 | we need" 53 | [scb] 54 | (get-json "/a/config" 55 | #(scb (js->clj % :keywordize-keys true)) 56 | (fn [] 57 | (log "Failed to get configuration from server") 58 | (scb {})))) 59 | 60 | (defn get-files [path files-cb error-cb] 61 | (log path) 62 | (get-json "/a/files" {:path path} files-cb error-cb)) 63 | 64 | (defn tag-attach [path tag done] 65 | (log "Attaching " tag " to path: " path) 66 | (http-post "/a/apply-tag" {:tag tag :target path} 67 | #(done true) 68 | #(done false))) 69 | 70 | (defn get-file-listing 71 | "Gets the file listing and posts it to the given channel" 72 | [path scb] 73 | (get-files path 74 | scb 75 | #(log %))) 76 | 77 | (defn request-tags 78 | "Gets the tags available on the server and notified through the given channel" 79 | [tags-chan] 80 | (get-json "/a/tags" 81 | (fn [data] 82 | (go (>! tags-chan (js->clj data :keywordize-keys true)))) 83 | (fn [] 84 | (log "Failed to load tags")))) 85 | 86 | (defn add-tag 87 | "Add a tag by posting request to remote end" 88 | [name target scb ecb] 89 | (http-post "/a/tags" {:name name :target target} 90 | scb 91 | ecb)) 92 | 93 | (defn remove-tag 94 | "Remote a tag by posting a request" 95 | [name scb ecb] 96 | (http-delete (str "/a/tags/" name) scb ecb)) 97 | 98 | (defn merge-listing 99 | "Merge all the information about downloads and tags from the two separate sources 100 | into one listing" 101 | [listing tags downloads current-path] 102 | (let [tags (into {} (for [t tags] [(:name t) t])) 103 | 104 | ;; strip out the string from the last . to the end of the path, and return true if it matches current path 105 | current-path? (fn [p] 106 | (let [last-period (.lastIndexOf p "./") 107 | last-slash (.lastIndexOf p "/") 108 | path (.substring p last-period last-slash)] 109 | (= path current-path))) 110 | 111 | ;; Clean out the path and pick the last component of it 112 | filename (fn [p] 113 | (last (split p #"/"))) 114 | dls (into {} (for [dl (:active downloads) :when (current-path? (:from dl))] [(filename (:from dl)) dl])) 115 | attach-tag (fn [l] 116 | (if-let [tag-name (:tag l)] 117 | (assoc l :tag (get tags tag-name)) 118 | l)) 119 | attach-dl (fn [l] 120 | (if-let [dl (get dls (:name l))] 121 | (let [ds (if (:download-status dl) 122 | {:available (:download-status dl)} 123 | {:waiting nil})] 124 | (assoc l :download ds)) 125 | l))] 126 | (->> listing 127 | ;; Associate tag information 128 | (map #(->> % 129 | attach-tag 130 | attach-dl))))) 131 | 132 | (def path-refresh-timeout (atom nil)) 133 | (def path-refresh-was-canceled (atom false)) 134 | (def path-refresh-active-path (atom nil)) 135 | 136 | (defn path-refresh 137 | "Continously query the given path at regular intervals, should be cancellable" 138 | [path f] 139 | (let [to (js/setTimeout (fn [] 140 | (get-file-listing path 141 | (fn [list] 142 | ;; Make sure this response is for our currently active path 143 | ;; and not a stale one 144 | (when (and (not @path-refresh-was-canceled) 145 | (= path @path-refresh-active-path)) 146 | (f list) 147 | (path-refresh path f))))) 148 | 2000)] 149 | (reset! path-refresh-active-path path) 150 | (reset! path-refresh-was-canceled false) 151 | (reset! path-refresh-timeout to))) 152 | 153 | (defn cancel-path-refresh 154 | "Cancel an active path refresh loop" 155 | [] 156 | (when @path-refresh-timeout 157 | (log "Clearing timeout: " @path-refresh-timeout) 158 | (js/clearTimeout @path-refresh-timeout) 159 | (reset! path-refresh-timeout nil) 160 | (reset! path-refresh-was-canceled true) 161 | (log "Path refresh was cancelled!"))) 162 | 163 | (defn full-page 164 | "Full page om component" 165 | [app owner] 166 | (reify 167 | om/IInitState 168 | (init-state [_] 169 | {:is-loading false 170 | :sort-order [:modified false] 171 | :path-chan (chan) 172 | :listing-chan (chan) 173 | :sort-order-chan (chan) 174 | :tags-chan (chan) 175 | :downloads-chan (chan) 176 | :apply-chan (chan) 177 | :add-chan (chan) 178 | :remove-chan (chan) 179 | :pusher-modal-chan (chan) 180 | :modal-chan (chan)}) 181 | om/IWillMount 182 | (will-mount [_] 183 | (let [path-chan (om/get-state owner :path-chan) 184 | sort-order-chan (om/get-state owner :sort-order-chan) 185 | downloads-chan (om/get-state owner :downloads-chan) 186 | tags-chan (om/get-state owner :tags-chan) 187 | apply-chan (om/get-state owner :apply-chan) 188 | add-chan (om/get-state owner :add-chan) 189 | remove-chan (om/get-state owner :remove-chan)] 190 | ;; Start the loop to listen to user clicks on the quick shortcuts 191 | (go (loop [] 192 | (let [path ( (keep-indexed #(if (= fname (:name %2)) %1) lst) 231 | first)] 232 | (log idx) 233 | (assoc-in lst [idx :tag] name))))))))) 234 | 235 | (recur))) 236 | ;; Add chan 237 | (go (loop [] 238 | (let [[name path] (clj d :keywordize-keys true) t))))) 245 | #(log "Failed to add tag!"))) 246 | (recur))) 247 | ;; go loop for removing tags 248 | (go (loop [] 249 | (let [tag (! path-chan ".")) 269 | 270 | ;; start download manager 271 | ;; 272 | (start-listening-for-downloads downloads-chan))) 273 | om/IRenderState 274 | (render-state [this state] 275 | (let [[sort-key sort-asc] (om/get-state owner :sort-order) 276 | sort-list (fn [listing] ((sort-key sort-funcs) listing sort-asc))] 277 | (dom/div #js {:className "page"} 278 | (dom/div #js {:className "clearfix"} 279 | (dom/h3 #js {:className "name"} (:name app)) 280 | (om/build download-activity-monitor (:downloads app))) 281 | (dom/div #js {:className "actions-menu"} 282 | (dom/a #js {:href "#" 283 | :onClick #(go (>! (:pusher-modal-chan state) ""))} "Upload")) 284 | ;; Setup current path view 285 | ;; 286 | (om/build current-path 287 | (:current-path app) 288 | {:opts {:path-chan (:path-chan state)}}) 289 | ;; Set the sort order view 290 | ;; 291 | (om/build sort-order 292 | (:sort-order state) 293 | {:opts {:sort-order-chan (:sort-order-chan state)}}) 294 | ;; listing view 295 | ;; 296 | (om/build listing {:listing (-> (:listing app) 297 | (merge-listing (:tags app) (:downloads app) (:current-path app)) 298 | sort-list) 299 | :current-path (:current-path app)} 300 | {:opts {:path-chan (:path-chan state) 301 | :modal-chan (:modal-chan state)}}) 302 | ;; setup our tags modal 303 | ;; 304 | (om/build tags-modal 305 | (:tags app) 306 | {:opts {:modal-chan (:modal-chan state) 307 | :apply-chan (:apply-chan state) 308 | :remove-chan (:remove-chan state) 309 | :add-chan (:add-chan state)}}) 310 | 311 | ;; The Modal to accept content URLs 312 | ;; 313 | (om/build content-pusher-modal 314 | (:tags app) ;; we need to pass in some cursor to avoid an issue 315 | {:opts {:pusher-modal-chan (:pusher-modal-chan state)}}) 316 | 317 | (when (:is-loading state) 318 | (dom/div #js {:className "dim"} 319 | (dom/div #js {:className "loader"} "")))))))) 320 | 321 | (defn startup 322 | "Page initialization" 323 | [] 324 | (om/root full-page app-state {:target (. js/document (getElementById "app"))})) 325 | 326 | (ready (startup)) 327 | -------------------------------------------------------------------------------- /src-cljs/dakait/util.cljs: -------------------------------------------------------------------------------- 1 | (ns dakait.util 2 | ) 3 | 4 | (defn format-file-size 5 | "Make a formatted string out of the given number of bytes" 6 | [n] 7 | (let [[size postfix] (cond 8 | (< n 1000) [n "B"] 9 | (< n 1000000) [(/ n 1000) "K"] 10 | (< n 1000000000) [(/ n 1000000.0) "M"] 11 | (< n 1000000000000) [(/ n 1000000000.0) "G"] 12 | (< n 1000000000000000) [(/ n 1000000000000.0) "T"] 13 | :else [n "B"]) 14 | fixedSize (if (< n 1000000) 0 1)] 15 | (apply str [(.toFixed size fixedSize) postfix]))) 16 | 17 | (defn- sub-hour-format-date 18 | [n] 19 | (let [now (quot (.getTime (js/Date.)) 1000) 20 | diffInSecs (- now n)] 21 | (cond 22 | (< diffInSecs 5) "Less than 5 seconds ago" 23 | (< diffInSecs 10) "Less than 10 seconds ago" 24 | (< diffInSecs 60) "Less than a minute ago" 25 | :else (str (inc (quot diffInSecs 60)) " minutes ago")))) 26 | 27 | (defn format-date 28 | "Given a time stamp in seconds since epoch, returns a nicely formated time" 29 | [n] 30 | (let [dt (* n 1000) 31 | now (.getTime (js/Date.)) 32 | diffInSecs (quot (- now dt) 1000) 33 | diffInHours (quot diffInSecs 3600)] 34 | (cond 35 | (< diffInHours 1) (sub-hour-format-date n) 36 | (< diffInHours 2) "An hour ago" 37 | (< diffInHours 24) (str diffInHours " hours ago") 38 | (< diffInHours 48) "A day ago" 39 | (< diffInHours 168) (str (quot diffInHours 24) " days ago") 40 | :else (.toDateString (js/Date. dt))))) 41 | 42 | (defn duration-since 43 | "Given a time stamp (time in seconds since epoch), returns how much time in seconds has passed since" 44 | [n] 45 | (let [now (quot (.getTime (js/Date.)) 1000)] 46 | (- now n))) 47 | -------------------------------------------------------------------------------- /src/dakait/assocs.clj: -------------------------------------------------------------------------------- 1 | ;; assocs.clj 2 | ;; Association handling 3 | ;; 4 | 5 | (ns dakait.assocs 6 | (:use dakait.config 7 | [dakait.util :only (join-path)] 8 | [clojure.tools.logging :only (info error)]) 9 | (:require 10 | [clojure.java.io :as io] 11 | [clojure.data.json :as json])) 12 | 13 | 14 | ;; File association functions, we need to store current associations for files 15 | ;; that the user has tagged so that user may return tags back to the user. 16 | ;; 17 | ;; The way staging works is this: 18 | ;; 1. The download of a file is indepdent of its tag 19 | ;; 2. The tags may be changed while the file is downloading 20 | ;; or has been downloaded. 21 | ;; 3. If the file has been downloaded the new tag moves the file to the tag's 22 | ;; location. 23 | ;; 4. If the file is still being downloaded, the tagging operation is just delayed 24 | ;; till the file is downloaded and then staged at its appropriate location based 25 | ;; on the tag. 26 | ;; 27 | 28 | (def assocs (atom {})) 29 | (def assocs-file (atom "")) 30 | 31 | (defn- figure-assocs-file 32 | "Determine where the assocs file should be" 33 | [] 34 | (reset! assocs-file (join-path (config :config-data-dir) "assocs.json"))) 35 | 36 | (defn load-associations 37 | "Load associations from our configuration file" 38 | [] 39 | (figure-assocs-file) 40 | (info "Associations file: " @assocs-file) 41 | (when (.exists (io/file @assocs-file)) 42 | (->> @assocs-file 43 | slurp 44 | json/read-str 45 | (reset! assocs)))) 46 | 47 | (defn- flush-to-disk 48 | "Write new set of associations to disk" 49 | [assocs] 50 | (->> assocs 51 | json/write-str 52 | (spit @assocs-file))) 53 | 54 | (defn- assoc-key 55 | "Given a file in the remote file system, append the server info for a unique key" 56 | [path] 57 | (str (config :username) "@" (config :sftp-host) ":" path)) 58 | 59 | (defn add-association 60 | "Add a new association" 61 | [tag path] 62 | (let [key (assoc-key path)] 63 | (reset! assocs (assoc @assocs key tag))) 64 | (flush-to-disk @assocs)) 65 | 66 | (defn get-association 67 | "Get an already set association, nil otherwise" 68 | [path] 69 | (let [key (assoc-key path)] 70 | (get @assocs key))) 71 | -------------------------------------------------------------------------------- /src/dakait/config.clj: -------------------------------------------------------------------------------- 1 | (ns dakait.config 2 | (:use [dakait.util :only [join-path]] 3 | [clojure.walk :only [keywordize-keys]]) 4 | (:require [clojure.data.json :as json])) 5 | 6 | (def loaded-config (atom {})) 7 | (def config-file (join-path (System/getProperty "user.dir") "config.json")) 8 | (def defaults { :server-name "Server" 9 | :sftp-port 22 10 | :concurrency 4 11 | :use-ipv6 false 12 | :local-base-path "." 13 | :username (System/getProperty "user.name") 14 | :push-path "." 15 | :base-path "." }) 16 | 17 | (defn config [k] 18 | "Get the associated configuration value, if the config doesn't exist, return the default" 19 | (get @loaded-config k (k defaults))) 20 | 21 | (defn- make-sure-required-exist [] 22 | "Makes sure that required properties exist" 23 | (let [required '(:config-data-dir 24 | :staging-dir 25 | :sftp-host :private-key)] 26 | (doseq [k required] 27 | (when (nil? (config k)) 28 | (throw (Exception. (str "Required configuration: " (name k)))))))) 29 | 30 | 31 | (defn load-and-validate [] 32 | "Load the configuration setting up appropriate defaults and check if we have 33 | all the things we need" 34 | (reset! loaded-config 35 | (->> config-file 36 | slurp 37 | json/read-str 38 | keywordize-keys)) 39 | 40 | (make-sure-required-exist) 41 | (let [props '(:config-data-dir 42 | :local-base-path 43 | :concurrency 44 | :server-name :sftp-host 45 | :sftp-port :username :base-path :private-key)] 46 | (println "Using configuration: " config-file) 47 | (doseq [p props] 48 | (println (name p) ":" (config p))))) 49 | -------------------------------------------------------------------------------- /src/dakait/downloader.clj: -------------------------------------------------------------------------------- 1 | ;; downloader.clj 2 | ;; async sub-service to download stuff 3 | ;; 4 | 5 | (ns dakait.downloader 6 | (:use dakait.config 7 | [dakait.util :only (join-path)] 8 | [clojure.core.match :only (match)] 9 | [clojure.core.async :only (thread)] 10 | [clojure.tools.logging :only (info warn error)]) 11 | (:require 12 | [clj-ssh.ssh :as ssh] 13 | [clojure.java.io :as io] 14 | [me.raynes.conch.low-level :as sh] 15 | [clojure.data.json :as json])) 16 | 17 | (def active-downloads (atom [])) 18 | (def download-queue (atom (clojure.lang.PersistentQueue/EMPTY))) 19 | (def download-states (atom {})) ;; directly modified by download threads 20 | 21 | (defn- escape-for-commandline 22 | "Replace all spaces in string with command line escaped spaces (with slash before every space) 23 | based on an answer here: http://stackoverflow.com/a/20053121/73544 best thing to do is to escape 24 | all the things" 25 | [s] 26 | (->> s 27 | (map #(if (Character/isLetterOrDigit %) 28 | (str %) 29 | (str "\\" %))) 30 | (apply str))) 31 | 32 | (defn- os-proof 33 | "Operating system specific filename formatting, os x accepts the scp commands as a list 34 | whereas linux accepts it as a single argument, we need to quote dest for linux in case there are 35 | spaces in the destination since its passed to the shell as it is" 36 | [s] 37 | (let [os (clojure.string/lower-case (System/getProperty "os.name"))] 38 | (cond 39 | (= os "mac os x") s 40 | (= os "linux") (escape-for-commandline s) 41 | :else (throw (Exception. "scp handling is not implemented for this os"))))) 42 | 43 | 44 | (defn- make-download-command 45 | "Makes operating system specific script + scp command" 46 | [src dest tmp-file] 47 | (let [os (clojure.string/lower-case (System/getProperty "os.name")) 48 | os-src (os-proof src) 49 | os-dest (os-proof dest) 50 | scp-command (list "scp" 51 | "-i" (config :private-key) ;; identity file 52 | (if (config :use-ipv6) "-6" "-4") ;; use appropriate flag to force IP version selection 53 | "-B" ;; batch run 54 | "-r" ;; recursive if directory 55 | "-o" "StrictHostKeyChecking=no" 56 | "-P" (config :sftp-port) ;; the port to use 57 | (str (config :username) "@" (config :sftp-host) ":\"" os-src "\"") ;; source 58 | os-dest)] 59 | (info "making download command, src: " os-src ", dest: " os-dest) 60 | (cond 61 | (= os "mac os x") (concat (list "script" "-t" "0" "-q" tmp-file) 62 | scp-command) 63 | (= os "linux") (list 64 | "script" "-f" "-e" "-q" 65 | "-c" (apply str (interpose " " scp-command)) 66 | tmp-file) 67 | :else (throw (Exception. "scp handling is not implemented for this os"))))) 68 | 69 | 70 | ;; Download management 71 | ;; 72 | (defn- download 73 | "Download the given file or directory to the given directory" 74 | [src dest] 75 | (.mkdirs (io/file dest)) ;; Make sure the destination directory exists 76 | (let [tmp-file (.getAbsolutePath (java.io.File/createTempFile "downloader" "txt")) 77 | args (make-download-command src dest tmp-file) 78 | update-to-map (fn [s] 79 | (when-not (empty? s) 80 | (let [parts (reverse (remove empty? (clojure.string/split s #"\s")))] 81 | (when (= (first parts) "ETA") 82 | (let [[eta rate dl pc & junk] (rest parts)] 83 | {:percent-complete pc 84 | :downloaded dl 85 | :rate rate 86 | :eta eta})))))] 87 | ;; scp command shows ETA as the last thing when downloading, but doesn't when done. 88 | ;; make sure to drop the first element when its ETA, we reverese the components to make 89 | ;; it easier on us to destruct them later 90 | [(future (do 91 | (try 92 | (info "Starting process: " args) 93 | (let [p (apply sh/proc (map str args))] 94 | (with-open [rdr (io/reader (:out p))] 95 | (doseq [line (line-seq rdr)] 96 | (let [state-map (update-to-map line)] 97 | (swap! download-states assoc src state-map)))) 98 | (sh/exit-code p)) 99 | (catch Exception e 100 | (info "Download process failed: " (.getMessage e)) 101 | (.printStackTrace e) 102 | 0) 103 | (finally 104 | (io/delete-file tmp-file true))))) src dest])) 105 | 106 | (defn- download-manager 107 | "Runs in an end-less loop looking for new down load requests and dispatching them" 108 | [concurrency] 109 | (info "Starting download manager") 110 | (thread (while true 111 | ;; check if we have room for another download, if we do check if we have a task waiting 112 | ;; if so, start it up 113 | (try 114 | ; start any new tasks that need to be started if we have room 115 | ; 116 | (when (< (count @active-downloads) concurrency) 117 | (when-let [next-task (peek @download-queue)] 118 | (swap! download-queue pop) 119 | (let [[src dest f] next-task 120 | p (download src dest)] 121 | (info "Process is: " (apply str p)) 122 | ; false indicates that the user has not been notified about this completion yet 123 | (let [new-dl-state (conj (vec p) f false)] 124 | (swap! active-downloads conj new-dl-state))))) 125 | 126 | ; call callbacks on any completed but not triggered tasks 127 | ; 128 | (swap! active-downloads 129 | (fn [dls] 130 | (map #(match [%] 131 | [[(t :guard realized?) s d cb false]] (do 132 | (cb (= @t 0)) 133 | [t s d cb true]) 134 | :else %) dls))) 135 | 136 | ; Remove any completed futures from our active downloads list 137 | ; 138 | (swap! active-downloads #(remove last %)) 139 | 140 | (catch Exception e 141 | (warn "Exception in downloader thread: " (.getMessage e)) 142 | (.printStackTrace e)) 143 | (finally 144 | (Thread/sleep 1000)))))) 145 | 146 | (defn downloads-in-progress 147 | "Gets all active downloads" 148 | [] 149 | (let [ad @active-downloads 150 | ds @download-states] 151 | (map (fn [[fut src dest & rest]] 152 | {:from src 153 | :to dest 154 | :download-status (ds src)}) ad))) 155 | 156 | (defn downloads-pending 157 | "Get all the pending downloads as a seq" 158 | [] 159 | (seq @download-queue)) 160 | 161 | (defn start-download 162 | "Start a download for the given file" 163 | ([src dest] 164 | (start-download src dest (fn [s]))) 165 | ([src dest f] 166 | (info "Queuing download, source: " src ", destination: " dest) 167 | (swap! download-queue conj [src dest f]))) 168 | 169 | (defn run 170 | "Run the downloader loop" 171 | [] 172 | (download-manager (config :concurrency))) 173 | -------------------------------------------------------------------------------- /src/dakait/files.clj: -------------------------------------------------------------------------------- 1 | (ns dakait.files 2 | (:use compojure.core 3 | dakait.config 4 | [clojure.core.async :only (thread)] 5 | [dakait.util :only (join-path)] 6 | [clojure.tools.logging :only (info error)]) 7 | (:require 8 | [clojure.java.io :as io] 9 | [clj-ssh.ssh :as ssh])) 10 | 11 | (def ssh-agent (atom nil)) 12 | (def ssh-session (atom nil)) 13 | 14 | (defn- agent-with-identity [] 15 | "Get an agent with properties setup correctly and the identity added from configuration" 16 | (when 17 | (or (nil? @ssh-agent) 18 | (not (ssh/ssh-agent? @ssh-agent))) 19 | (let [agent (ssh/ssh-agent {})] 20 | (ssh/add-identity agent { 21 | :private-key-path (config :private-key) }) 22 | (info "new agent") 23 | (reset! ssh-agent agent))) 24 | @ssh-agent) 25 | 26 | (def session-invalidator (atom nil)) 27 | (defn- reset-session-invalidate 28 | "This function acts as a hit on the session being active, every time this function is called 29 | session invalidation is reset, when the reset invalidation expires the ssh session is 30 | invalidated and set to nil" 31 | [] 32 | (swap! session-invalidator 33 | (fn [si] 34 | (when-not (nil? si) 35 | (future-cancel si)) 36 | (future 37 | (Thread/sleep 120000) 38 | (info "Invalidating session") 39 | (swap! ssh-session (fn [s] 40 | (ssh/disconnect s) 41 | nil)) 42 | (reset! session-invalidator nil))))) 43 | 44 | 45 | (defn- session [] 46 | "Get the currently active session, if one doesn't exist, create a new one and make sure the 47 | session is connected" 48 | (when (nil? @ssh-session) 49 | ;; Recreate our session if it doesn't exist or is not connected for some reason 50 | (let [host (config :sftp-host) 51 | user (config :username) 52 | port (config :sftp-port) 53 | agent (agent-with-identity) 54 | session (ssh/session agent host {:port port 55 | :username user 56 | :strict-host-key-checking :no})] 57 | (info "New session created with param: " host user port) 58 | (reset! ssh-session session) 59 | (ssh/connect session))) 60 | ;; seems to me the ssh/connected? seems to return true and then the channel creation fails with "Session not connected" 61 | ;; Explictely call connect every time the session is requested. 62 | (reset-session-invalidate) 63 | @ssh-session) 64 | 65 | (defn all-files [path] 66 | (let [file-type (fn [e] (if (.isDirectory e) "dir" "file")) 67 | file-size (fn [e] (.length e))] 68 | (seq (->> (.listFiles (clojure.java.io/file path)) 69 | (filter #(not (.isHidden %1))) 70 | (map (fn [e] { :name (.getName e) 71 | :type (file-type e) 72 | :size (file-size e)})))))) 73 | 74 | (defn list-remote-files 75 | "Get the list of all files at the given path" 76 | [path] 77 | (let [this-channel (ssh/ssh-sftp (session))] 78 | (ssh/with-channel-connection this-channel 79 | (when-not (nil? path) 80 | (ssh/sftp this-channel {} :cd path)) 81 | (ssh/sftp this-channel {} :ls)))) 82 | 83 | (defn all-remote-files [path] 84 | (let [query-path (join-path (config :base-path) path) 85 | now (quot (.getTime (java.util.Date.)) 1000) 86 | entries (list-remote-files query-path) 87 | not-hidden? (fn [e] (not= (.charAt (.getFilename e) 0) \.)) 88 | file-type (fn [e] (if (.isDir (.getAttrs e)) "dir" "file")) 89 | file-size (fn [e] (.getSize (.getAttrs e))) 90 | last-modified (fn [e] (-> e (.getAttrs) (.getMTime))) 91 | recent? (fn [e] (< (- now (last-modified e)) 10)) 92 | ] 93 | (->> entries 94 | (filter not-hidden?) 95 | (map (fn [e] { :name (.getFilename e) 96 | :type (file-type e) 97 | :modified (last-modified e) 98 | :recent (recent? e) 99 | :size (file-size e)}))))) 100 | 101 | -------------------------------------------------------------------------------- /src/dakait/handler.clj: -------------------------------------------------------------------------------- 1 | (ns dakait.handler 2 | (:use dakait.views 3 | dakait.files 4 | dakait.config 5 | dakait.mdns 6 | dakait.staging 7 | dakait.pusher 8 | org.httpkit.server 9 | compojure.core 10 | [compojure.handler :only (site)] 11 | [clojure.core.async :only(>! go alts! timeout)] 12 | [dakait.util :only (join-path)] 13 | [dakait.downloader :only (run start-download downloads-in-progress downloads-pending)] 14 | [dakait.assocs :only (load-associations add-association get-association)] 15 | [dakait.tags :only (load-tags get-all-tags add-tag remove-tag find-tag)] 16 | [clojure.tools.logging :only (info error)] 17 | [hiccup.middleware :only (wrap-base-url)]) 18 | (:require [ring.middleware.reload :as reload] 19 | [clojure.tools.cli :refer [parse-opts]] 20 | [compojure.route :as route] 21 | [clojure.data.json :as json])) 22 | 23 | ;; All the channels that need to be notified about any download status updates 24 | ;; 25 | (def ws-downloads-channels (atom [])) 26 | (def start-options (atom nil)) 27 | 28 | (defn as-json 29 | ([] 30 | (as-json {})) 31 | ([m] 32 | { :status 200 33 | :headers { "Content-Type" "application/json; charset=utf-8" } 34 | :body (json/write-str m) })) 35 | 36 | (defn as-json-error 37 | ([code error-message] 38 | { :status code 39 | :headers { "Content-Type" "application/json; charset=utf-8" } 40 | :body (json/write-str { :message error-message }) }) 41 | ([error-message] 42 | (as-json-error 503 error-message))) 43 | 44 | (defmacro do-with-cond [condition error msg & body] 45 | `(if ~condition 46 | (as-json-error ~error ~msg) 47 | (do 48 | ~@body))) 49 | 50 | (defn add-tag-info 51 | "Add tag info to all files provided given the base path" 52 | [base-path files] 53 | (map (fn [f] 54 | (let [name (:name f) 55 | file-path (join-path base-path name) 56 | ass (get-association file-path)] 57 | (if (nil? ass) 58 | f 59 | (assoc f :tag ass)))) files)) 60 | 61 | (defn random-html-color 62 | "Generate an awesome randome html color" 63 | [] 64 | (let [r (java.util.Random.)] 65 | (str "hsl(" (.nextInt r 360) ",50%,70%)"))) 66 | 67 | (defn handle-config 68 | "Returns the current required set of configuration to client" 69 | [] 70 | (as-json {:server-name (config :server-name)})) 71 | 72 | (defn handle-files 73 | "Fetch files for the given path" 74 | [path] 75 | (try 76 | (->> path 77 | all-remote-files 78 | (add-tag-info (join-path (config :base-path) path)) 79 | as-json) 80 | (catch Exception e 81 | (info "There was an error handling files request: " (.getMessage e)) 82 | (.printStackTrace e) 83 | (as-json-error (.getMessage e))))) 84 | 85 | (defn handle-apply-tag 86 | "Handle application of tags onto files" 87 | [tag target] 88 | (do-with-cond 89 | (or (nil? tag) (nil? target)) 400 "Tag and taget file needs to be specified" 90 | (let [tag-obj (find-tag tag) 91 | dest (:target tag-obj)] 92 | (do-with-cond 93 | (or (nil? tag-obj) (nil? dest)) 400 "The specified tag is invalid" 94 | (let [target-path (join-path (config :base-path) target) 95 | dest-path (join-path (config :local-base-path) (:target tag-obj))] 96 | ;; start the download 97 | ;; 98 | ;; (start-download target-path dest-path) 99 | ;; setup appropriate association 100 | ;; 101 | (stage-file target-path dest-path) 102 | (add-association tag target-path) 103 | (as-json)))))) 104 | 105 | (defn handle-get-all-tags [] 106 | (let [s (seq (get-all-tags))] 107 | (as-json 108 | (cond 109 | (nil? s) [] 110 | :else (map (fn [[n m]] (assoc m :name n)) s))))) 111 | 112 | (defn handle-create-tag 113 | "Handle creation of new tags" 114 | [name target] 115 | (let [random-color (random-html-color)] 116 | (add-tag name target random-color) 117 | (as-json {:name name 118 | :target target 119 | :color random-color}))) 120 | 121 | (defn handle-remove-tag 122 | "Handle deletion of tags" 123 | [name] 124 | (remove-tag name) 125 | (as-json)) 126 | 127 | (defn handle-active-downloads 128 | "Handle active downloads" 129 | [] 130 | (as-json {:active (downloads-in-progress) 131 | :pending (map (fn [d] {:from (first d) :to (second d)}) (downloads-pending))})) 132 | 133 | 134 | (defn handle-push 135 | "Handle a URL push" 136 | [url] 137 | (info "Going to try and push: " url) 138 | (try 139 | (if (do-push url) 140 | (as-json {:message "Pushed successfully"}) 141 | (as-json-error "Failed to complete action")) 142 | (catch Exception e 143 | (as-json-error (.getMessage e))))) 144 | 145 | (defn ws-downloads-pusher 146 | "Pushes downloads status every so often" 147 | [] 148 | (go (while true 149 | (alts! [(timeout 1000)]) 150 | (let [msg (json/write-str {:active (downloads-in-progress) 151 | :pending (map (fn [d] {:from (first d) :to (second d)}) (downloads-pending))})] 152 | (doseq [c @ws-downloads-channels] 153 | (send! c msg)))))) 154 | 155 | ;; This end-point handles all incoming websocket connections, or long polling connections 156 | (defn ws-downloads 157 | "Handle incoming websocket connections for downloads updates" 158 | [request] 159 | (with-channel request channel 160 | (on-close channel (fn [status] 161 | (println "Websocket channel is going away!") 162 | (swap! ws-downloads-channels 163 | (fn [chs] (remove #(= % channel) chs))))) 164 | (println "New downloads notification") 165 | (swap! ws-downloads-channels #(cons channel %)))) 166 | 167 | (defroutes app-routes 168 | (GET "/" [] (if (:debug @start-options) 169 | (debug-index-page) 170 | (index-page))) 171 | (GET "/a/config" [] 172 | (handle-config)) 173 | (GET "/a/files" {params :params } 174 | (handle-files (:path params))) 175 | (GET "/a/tags" [] (handle-get-all-tags)) 176 | (POST "/a/tags" {params :params} 177 | (handle-create-tag (:name params) (:target params))) 178 | (DELETE "/a/tags/:name" [name] 179 | (handle-remove-tag name)) 180 | (POST "/a/apply-tag" {params :params } 181 | (handle-apply-tag (:tag params) (:target params))) 182 | (GET "/a/downloads" [] (handle-active-downloads)) 183 | (GET "/ws/downloads" [] ws-downloads) 184 | (GET "/a/params" {params :params} (pr-str params)) 185 | (POST "/a/push" {params :params} 186 | (handle-push (:url params))) 187 | (route/resources "/") 188 | (route/not-found "Not Found")) 189 | 190 | 191 | (def app 192 | (-> (site app-routes) 193 | wrap-base-url 194 | reload/wrap-reload)) 195 | 196 | (def cli-options 197 | [["-d" "--debug"]]) 198 | 199 | (defn do-init [& args] 200 | "Initialize program" 201 | (try 202 | (reset! start-options (:options (parse-opts args cli-options))) 203 | 204 | (load-and-validate) 205 | (load-tags (str (config :config-data-dir) "/tags.json")) 206 | (load-associations) 207 | (init-stager) 208 | (run) 209 | (ws-downloads-pusher) 210 | (catch Exception e 211 | (println "Program initialization failed: " (.getMessage e)) 212 | (System/exit 1)))) 213 | 214 | -------------------------------------------------------------------------------- /src/dakait/main.clj: -------------------------------------------------------------------------------- 1 | (ns dakait.main 2 | (:gen-class) 3 | (:use dakait.handler 4 | [org.httpkit.server :only (run-server)])) 5 | 6 | (defn -main [& args] 7 | "Application entry point" 8 | (apply do-init args) 9 | (println "Initialization complete...") 10 | (run-server app {:port 3000})) 11 | 12 | -------------------------------------------------------------------------------- /src/dakait/mdns.clj: -------------------------------------------------------------------------------- 1 | (ns dakait.mdns 2 | (:import (javax.jmdns JmDNS 3 | ServiceInfo))) 4 | 5 | (def ^{:private true} service (atom nil)) 6 | 7 | (defn publish-service 8 | "Register a mDNS service with given information" 9 | [port] 10 | (let [service-info (ServiceInfo/create 11 | "_dakait._tcp.local." 12 | "Dakait" 13 | port 14 | "SFTP File download and management utility") 15 | mdns (JmDNS/create)] 16 | (.registerService mdns service-info) 17 | (reset! service mdns))) 18 | 19 | -------------------------------------------------------------------------------- /src/dakait/pusher.clj: -------------------------------------------------------------------------------- 1 | ;; pusher.clj 2 | ;; Helps push stuff on to the server 3 | 4 | (ns dakait.pusher 5 | (:use [dakait.config :only [config]] 6 | [dakait.util :only [filename join-path]]) 7 | (:require [me.raynes.conch :refer [with-programs]] 8 | [clj-ssh.ssh :as ssh] 9 | [org.httpkit.client :as http])) 10 | 11 | (defn init-pusher 12 | "Initilize stuff" 13 | [] 14 | ;; We need to make sure we have curl available and in path 15 | ) 16 | 17 | (defn- push-to-server 18 | "Push the given file to server" 19 | [file {:keys [host port path private-key]}] 20 | (let [agent (ssh/ssh-agent {})] 21 | (ssh/add-identity agent {:private-key-path private-key}) 22 | (let [session (ssh/session agent host {:strict-host-key-checking :no})] 23 | (ssh/with-connection session 24 | (let [channel (ssh/ssh-sftp session)] 25 | (ssh/with-channel-connection channel 26 | (ssh/sftp channel {} :cd path) 27 | (ssh/sftp channel {} :put file (filename file)))))))) 28 | 29 | (defn- parse-content-disp 30 | "Parses the content disposition header" 31 | [header] 32 | (when-let [m (re-find #"(?i)Filename=\"(.*)\"" header)] 33 | (second m))) 34 | 35 | 36 | (defn- download-file 37 | "Download the given file to the given path, return the fully formed 38 | path of the file or nil" 39 | [url path] 40 | (let [{:keys [status headers body error]} @(http/get url {:user-agent "Dakait" 41 | :as :byte-array})] 42 | (if (= status 200) 43 | (let [filename (if (:content-disposition headers) 44 | (parse-content-disp (:content-disposition headers)) 45 | (filename url)) 46 | out-path (join-path path filename)] 47 | (with-open [w (clojure.java.io/output-stream out-path)] 48 | (.write w body)) 49 | out-path) 50 | nil))) 51 | 52 | (defn do-push 53 | [url] 54 | ;; First bring down the file to local computer 55 | (if-let [path (download-file url (config :config-data-dir))] 56 | (do 57 | (push-to-server path {:host (config :sftp-host) 58 | :port (config :sftp-port) 59 | :path (config :push-path) 60 | :private-key (config :private-key)}) 61 | (clojure.java.io/delete-file path) 62 | true) 63 | false)) 64 | -------------------------------------------------------------------------------- /src/dakait/staging.clj: -------------------------------------------------------------------------------- 1 | ;; staging.clj 2 | ;; Staging support, all downloads are downloaded to the configured staging area, when the download 3 | ;; completes, the files are moved to the destination path, if a file is already in the destination area 4 | ;; and was successfully downloaded, this module also helps locate it and help move it to another destination 5 | ;; path 6 | ;; 7 | 8 | (ns dakait.staging 9 | (:use dakait.config 10 | [dakait.util :only (join-path map-vals filename)] 11 | [clojure.core.match :only (match)] 12 | [dakait.downloader :only (start-download)] 13 | [clojure.tools.logging :only (info error)]) 14 | (:require 15 | [me.raynes.conch :refer [with-programs]] 16 | [clojure.walk :refer [keywordize-keys]] 17 | [clojure.java.io :as io] 18 | [clojure.data.json :as json])) 19 | 20 | 21 | (def staged (atom {})) 22 | (def staged-info-file (atom nil)) 23 | 24 | (defn- flush-to-disk 25 | "Write the current state of staged information to disk" 26 | [] 27 | (->> @staged 28 | json/write-str 29 | (spit @staged-info-file))) 30 | 31 | (defn- read-from-disk 32 | "Read current state from disk" 33 | [] 34 | (when (.exists (io/file @staged-info-file)) 35 | (->> @staged-info-file 36 | slurp 37 | json/read-str 38 | (map-vals #(let [m (keywordize-keys %)] ;; All keys need to be keywordized, and the :download-state's value needs to be keywordized 39 | (assoc m :download-state (keyword (:download-state m)))))))) 40 | 41 | 42 | (defn init-stager 43 | "Initializes the stager" 44 | [] 45 | (reset! staged-info-file (join-path (config :config-data-dir) "staging.json")) 46 | (reset! staged (read-from-disk))) 47 | 48 | 49 | (defn- move-to-dir 50 | "Move file from given source to given destination" 51 | [src dest] 52 | (.mkdirs (io/file dest)) 53 | (with-programs [mv] 54 | (info "Moving " src " -> " dest) 55 | (let [r (mv src dest {:verbose true})] 56 | (info "Move status: " r)))) 57 | 58 | (defn stage-file 59 | "Helps stage the file, triggers download of the file to the staged configuration area, once the 60 | download finishes the file is moved to appropriate area" 61 | [target-path dest-path] 62 | ;; Depending on what our download state is we do a few things 63 | ;; check the file download status 64 | (match [(get-in @staged [target-path :download-state])] 65 | [:downloaded] 66 | (do 67 | (info "The file has been downloaded already, so will move it") 68 | (let [filen (filename target-path) 69 | src (join-path (get-in @staged [target-path :dest]) filen)] 70 | (move-to-dir src dest-path) 71 | (swap! staged assoc-in [target-path :dest] dest-path))) 72 | [:downloading] 73 | (do 74 | (info "The file is still in progress, just updating path") 75 | (swap! staged assoc-in [target-path :dest] dest-path)) 76 | [nil] 77 | (do 78 | (info "There is no download state, start one") 79 | (start-download target-path (config :staging-dir) 80 | (fn [code] 81 | (info "File download completed! performing post download stuff") 82 | (info "Process completion code is" code) 83 | (if code ;; this file was successfully downloaded, move the file to destination 84 | (let [info (get @staged target-path)] 85 | (move-to-dir (join-path (config :staging-dir) (filename target-path)) (:dest info)) 86 | (swap! staged assoc-in [target-path :download-state] :downloaded)) 87 | (swap! staged assoc-in [target-path :download-state] nil)) ;; don't set any state, back to nil so the download could be re-triggered 88 | (flush-to-disk))) 89 | (swap! staged assoc target-path {:download-state :downloading 90 | :dest dest-path}))) 91 | 92 | ;; Finally flush state to disk 93 | (flush-to-disk)) 94 | -------------------------------------------------------------------------------- /src/dakait/tags.clj: -------------------------------------------------------------------------------- 1 | ;; tags.clj 2 | ;; Tags management, no real database is used 3 | ;; disk files 4 | 5 | (ns dakait.tags 6 | (:use dakait.util) 7 | (:require 8 | [clojure.java.io :as io] 9 | [clojure.data.json :as json] 10 | [clojure.walk :refer [keywordize-keys]])) 11 | 12 | (def source-file (atom nil)) 13 | (def all-tags (atom {})) 14 | 15 | (defn- flush-to-disk 16 | "Flushes the current content of tags to disk" 17 | [] 18 | (when-not (nil? @source-file) 19 | (spit @source-file (json/write-str @all-tags)))) 20 | 21 | (defn load-tags 22 | "Load tags from source file" 23 | [file] 24 | (reset! source-file file) 25 | (when (.exists (io/as-file file)) 26 | (reset! all-tags (->> file 27 | slurp 28 | json/read-str 29 | (map-vals keywordize-keys))))) 30 | 31 | (defn get-all-tags 32 | "Return all tags that we know of" 33 | [] 34 | @all-tags) 35 | 36 | (defn add-tag 37 | "Add the given tag to the list of tags" 38 | [name target color] 39 | (reset! all-tags (assoc @all-tags name {:target target :color color})) 40 | (flush-to-disk)) 41 | 42 | (defn find-tag 43 | "Find the tag with the given name" 44 | [name] 45 | (get @all-tags name)) 46 | 47 | (defn remove-tag 48 | "Remove the given tag" 49 | [name] 50 | (when-not (nil? (find-tag name)) 51 | (reset! all-tags (dissoc @all-tags name)) 52 | (flush-to-disk))) 53 | -------------------------------------------------------------------------------- /src/dakait/util.clj: -------------------------------------------------------------------------------- 1 | (ns dakait.util 2 | (:require 3 | [clojure.java.io :as io])) 4 | 5 | (defn join-path 6 | "Join path elements together, if any of the path components start with a / 7 | the function assumes that the path is being reset to root and will ignore all parts 8 | before that" 9 | [p & parts] 10 | (let [p (if (= p "") "." p)] 11 | (.getPath (reduce #(if (.startsWith %2 "/") 12 | (io/file %2) 13 | (io/file %1 %2)) (io/file p) parts)))) 14 | 15 | (defn filename 16 | "Get the last component from the given path" 17 | [s] 18 | (->> (clojure.string/split s #"/") 19 | (remove empty?) 20 | last)) 21 | 22 | 23 | (defn map-vals 24 | "Maps the values of the given given map using the given function" 25 | [f col] 26 | (into {} (for [[k v] col] [k (f v)]))) 27 | 28 | -------------------------------------------------------------------------------- /src/dakait/views.clj: -------------------------------------------------------------------------------- 1 | (ns dakait.views 2 | (:use 3 | dakait.config 4 | [clojure.java.io :only (resource)] 5 | [clostache.parser :only (render)])) 6 | 7 | (defn render-template [file data] 8 | (render (slurp file) data)) 9 | 10 | (defn render-resource [file data] 11 | (-> file 12 | resource 13 | slurp 14 | (render data))) 15 | 16 | (defn index-page [] 17 | (render-resource "templates/index.mustache" {:title "Hello" 18 | :server-name (config :server-name) })) 19 | (defn debug-index-page [] 20 | (render-resource "templates/index.debug.mustache" {:title "Hello" 21 | :server-name (config :server-name)})) 22 | -------------------------------------------------------------------------------- /test/dakait/test/downloader.clj: -------------------------------------------------------------------------------- 1 | (ns dakait.test.downloader 2 | (:use clojure.test 3 | dakait.downloader)) 4 | -------------------------------------------------------------------------------- /test/dakait/test/tags.clj: -------------------------------------------------------------------------------- 1 | (ns dakait.test.tags 2 | (:use clojure.test 3 | dakait.tags)) 4 | 5 | (def not-nil? (complement nil?)) 6 | 7 | (deftest tags 8 | (testing "default state" 9 | (is (= (get-all-tags) {}))) 10 | 11 | (testing "Add tags" 12 | (add-tag "Test name" "/some/place" "#eee") 13 | (is (= (count (get-all-tags)) 1)) 14 | (let [t (find-tag "Test name")] 15 | (is (not-nil? t)) 16 | (is (= (:target t) "/some/place")) 17 | (is (= (:color t) "#eee")))) 18 | 19 | (testing "Remove tags" 20 | (add-tag "Test name" "/some/place" "#eee") 21 | (remove-tag "Test name") 22 | (is (zero? (count (get-all-tags))))) 23 | 24 | (testing "Remove correct tag" 25 | (add-tag "Test name" "/some/place" "#eee") 26 | (add-tag "Test name2" "/some/place" "#eee") 27 | (remove-tag "Test name") 28 | (is (= (count (get-all-tags)) 1)) 29 | (is (not-nil? (find-tag "Test name2"))))) 30 | -------------------------------------------------------------------------------- /test/dakait/test/util.clj: -------------------------------------------------------------------------------- 1 | (ns dakait.test.util 2 | (:use clojure.test 3 | dakait.util)) 4 | 5 | (defmacro join-path-as-string [& args] 6 | `(.toString (join-path ~@args))) 7 | 8 | (deftest util 9 | (testing "combines paths correctly" 10 | (is (= (join-path-as-string "/home" "test") "/home/test")) 11 | (is (= (join-path-as-string "/home" "/tmp") "/tmp")) 12 | (is (= (join-path-as-string "tmp") "tmp")) 13 | (is (= (join-path-as-string "." "/stuff") "/stuff")) 14 | (is (= (join-path-as-string "" "stuff") "./stuff")) 15 | (is (= (join-path-as-string "." "tmp") "./tmp"))) 16 | 17 | (testing "map-vals works correctly" 18 | (is (= (map-vals inc {:a 1 :b 1}) {:a 2 :b 2})) 19 | (is (= (map-vals keyword {:hello "world" :bye "world"}) {:hello :world :bye :world})))) 20 | 21 | 22 | --------------------------------------------------------------------------------