├── .bazelrc ├── .github └── workflows │ ├── release.yml │ └── test.yml ├── .gitignore ├── BUILD.bazel ├── LICENCE ├── README.md ├── WORKSPACE ├── cmd └── kipp │ ├── BUILD.bazel │ ├── flag.go │ ├── main.go │ ├── mime.go │ └── serve.go ├── database ├── BUILD.bazel ├── badger │ ├── BUILD.bazel │ └── badger.go ├── database.go └── sql │ ├── BUILD.bazel │ └── sql.go ├── deps.bzl ├── filesystem ├── BUILD.bazel ├── fs.go ├── local │ ├── BUILD.bazel │ ├── local.go │ └── local_test.go └── s3 │ ├── BUILD.bazel │ ├── reader.go │ └── s3.go ├── fs.go ├── fs_test.go ├── go.mod ├── go.sum ├── go_deps.bzl ├── hack └── workspace-status.sh ├── internal ├── databaseutil │ ├── BUILD.bazel │ └── parse.go ├── filesystemutil │ ├── BUILD.bazel │ └── parse.go ├── httputil │ ├── BUILD.bazel │ └── httputil.go └── x │ └── context │ ├── BUILD.bazel │ └── context.go ├── nogo_config.json ├── option.go ├── renovate.json ├── server.go ├── tools ├── BUILD.bazel └── tools.go └── web ├── css └── main.css ├── favicon-16x16.png ├── favicon-32x32.png ├── favicon.ico ├── index.html ├── js ├── filesize.min.js ├── jszip.min.js ├── main.js ├── musicmetadata.min.js ├── pica.min.js ├── qrious.min.js ├── qrious.min.js.map └── stackblur.min.js ├── private ├── css │ └── main.css ├── index.html └── js │ └── main.js ├── robots.txt └── sharex /.bazelrc: -------------------------------------------------------------------------------- 1 | build --stamp 2 | build --workspace_status_command hack/workspace-status.sh 3 | 4 | test --test_output=errors 5 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | release: 4 | types: [published] 5 | jobs: 6 | push: 7 | name: Push release tag 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v3 11 | - name: Install bazelisk 12 | run: curl -L "https://github.com/bazelbuild/bazelisk/releases/download/v1.7.4/bazelisk-linux-amd64" | install -D /dev/stdin "${GITHUB_WORKSPACE}/bin/bazel" 13 | - uses: docker/login-action@v2 14 | with: 15 | username: uhthomas 16 | password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} 17 | - uses: docker/login-action@v2 18 | with: 19 | registry: ghcr.io 20 | username: $GITHUB_ACTOR 21 | password: ${{ secrets.CR_PAT }} 22 | - run: bazel run //cmd/kipp:push 23 | env: 24 | GIT_COMMIT: ${{ github.sha }} 25 | GIT_REF: ${{ github.event.release.tag_name }} 26 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: [push] 3 | jobs: 4 | test: 5 | name: Test 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: actions/checkout@v3 9 | - name: Install bazelisk 10 | run: curl -L "https://github.com/bazelbuild/bazelisk/releases/download/v1.7.4/bazelisk-linux-amd64" | install -D /dev/stdin "${GITHUB_WORKSPACE}/bin/bazel" 11 | - run: bazel test //... 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /bazel-* 2 | user.bazelrc 3 | -------------------------------------------------------------------------------- /BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@bazel_gazelle//:def.bzl", "gazelle") 2 | 3 | # gazelle:prefix github.com/uhthomas/kipp 4 | gazelle(name = "gazelle") 5 | 6 | load("@com_github_bazelbuild_buildtools//buildifier:def.bzl", "buildifier") 7 | 8 | buildifier(name = "buildifier") 9 | 10 | load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test", "nogo") 11 | 12 | nogo( 13 | name = "nogo", 14 | config = "nogo_config.json", 15 | visibility = ["//visibility:public"], 16 | deps = [ 17 | "@org_golang_x_tools//go/analysis/passes/asmdecl:go_tool_library", 18 | "@org_golang_x_tools//go/analysis/passes/assign:go_tool_library", 19 | "@org_golang_x_tools//go/analysis/passes/atomic:go_tool_library", 20 | "@org_golang_x_tools//go/analysis/passes/bools:go_tool_library", 21 | "@org_golang_x_tools//go/analysis/passes/buildtag:go_tool_library", 22 | # "@org_golang_x_tools//go/analysis/passes/cgocall:go_tool_library", 23 | "@org_golang_x_tools//go/analysis/passes/composite:go_tool_library", 24 | "@org_golang_x_tools//go/analysis/passes/copylock:go_tool_library", 25 | "@org_golang_x_tools//go/analysis/passes/httpresponse:go_tool_library", 26 | "@org_golang_x_tools//go/analysis/passes/loopclosure:go_tool_library", 27 | "@org_golang_x_tools//go/analysis/passes/lostcancel:go_tool_library", 28 | "@org_golang_x_tools//go/analysis/passes/nilfunc:go_tool_library", 29 | "@org_golang_x_tools//go/analysis/passes/printf:go_tool_library", 30 | "@org_golang_x_tools//go/analysis/passes/shift:go_tool_library", 31 | "@org_golang_x_tools//go/analysis/passes/stdmethods:go_tool_library", 32 | "@org_golang_x_tools//go/analysis/passes/structtag:go_tool_library", 33 | "@org_golang_x_tools//go/analysis/passes/tests:go_tool_library", 34 | "@org_golang_x_tools//go/analysis/passes/unreachable:go_tool_library", 35 | "@org_golang_x_tools//go/analysis/passes/unsafeptr:go_tool_library", 36 | "@org_golang_x_tools//go/analysis/passes/unusedresult:go_tool_library", 37 | ], 38 | ) 39 | 40 | go_library( 41 | name = "go_default_library", 42 | srcs = [ 43 | "fs.go", 44 | "option.go", 45 | "server.go", 46 | ], 47 | importpath = "github.com/uhthomas/kipp", 48 | visibility = ["//visibility:public"], 49 | deps = [ 50 | "//database:go_default_library", 51 | "//filesystem:go_default_library", 52 | "//internal/databaseutil:go_default_library", 53 | "//internal/filesystemutil:go_default_library", 54 | "@com_github_gabriel_vasile_mimetype//:go_default_library", 55 | "@com_github_prometheus_client_golang//prometheus:go_default_library", 56 | "@com_github_prometheus_client_golang//prometheus/promhttp:go_default_library", 57 | "@com_github_zeebo_blake3//:go_default_library", 58 | ], 59 | ) 60 | 61 | go_test( 62 | name = "go_default_test", 63 | srcs = ["fs_test.go"], 64 | embed = [":go_default_library"], 65 | deps = ["//database:go_default_library"], 66 | ) 67 | 68 | filegroup( 69 | name = "web", 70 | srcs = glob(["web/**"]), 71 | visibility = ["//visibility:public"], 72 | ) 73 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | {one line to give the program's name and a brief idea of what it does.} 635 | Copyright (C) {year} {name of author} 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | {project} Copyright (C) {year} {fullname} 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # kipp 2 | [![Go Reference](https://pkg.go.dev/badge/github.com/uhthomas/kipp.svg)](https://pkg.go.dev/github.com/uhthomas/kipp) 3 | [![Go Report Card](https://goreportcard.com/badge/github.com/uhthomas/kipp)](https://goreportcard.com/report/github.com/uhthomas/kipp) 4 | 5 | ## Getting started 6 | The easiest way to get started with kipp is by using the image published to 7 | [Docker Hub](https://hub.docker.com/repository/docker/uhthomas/kipp). The 8 | service is then available simply by running: 9 | ``` 10 | docker pull uhthomas/kipp 11 | docker run uhthomas/kipp 12 | ``` 13 | 14 | ## Databases 15 | Databases can be configured using the `--database` flag. The flag requires 16 | the input be parsable as a URL. See the [url.Parse](https://golang.org/pkg/net/url/#Parse) 17 | docs for more info. 18 | 19 | ### [Badger](https://github.com/dgraph-io/badger) 20 | Badger is a fast, embedded database which is great for single instances. 21 | 22 | ### SQL 23 | Kipp uses a generic SQL driver, but currently only loads: 24 | * [PostgreSQL](https://www.postgresql.org/) 25 | 26 | As long as a database supports Go's [sql](https://golang.org/pkg/database/sql/) 27 | package, it can be used. Please file an issue for requests. 28 | 29 | ## File systems 30 | File systems can be configured using the `--filesystem` flag. The flag requires 31 | the input be parsable as a URL. See the [url.Parse](https://golang.org/pkg/net/url/#Parse) 32 | docs for more info. 33 | 34 | ### Local (your local file system) 35 | The local filesystem does not require any special formatting, and can be used 36 | like a regular path such 37 | 38 | ``` 39 | --filesystem /path/to/files 40 | ``` 41 | 42 | ### [AWS S3](https://aws.amazon.com/s3/) 43 | AWS S3 requires the `s3` scheme, and has the following syntax: 44 | 45 | ``` 46 | --filesystem s3://some-token:some-secret@some-region/some-bucket?endpoint=some-endpoint. 47 | ``` 48 | 49 | The `region` and `bucket` are required. 50 | 51 | The [user info](https://tools.ietf.org/html/rfc2396#section-3.2.2) section is 52 | optional, if present, will create new static credentials. Otherwise, the default 53 | AWS SDK credentials will be used. 54 | 55 | The `endpoint` is optional, and will use the default AWS endpoint if not present. 56 | This is useful for using S3-compatible services such as: 57 | * [Google Cloud Storage](https://cloud.google.com/storage) - storage.googleapis.com 58 | * [Linode Object Storage](https://www.linode.com/products/object-storage/) - linodeobjects.com 59 | * [Backblaze B2](https://www.backblaze.com/b2/cloud-storage.html) - backblazeb2.com 60 | * [DigitalOcean Spaces](https://www.digitalocean.com/products/spaces/) - digitaloceanspaces.com 61 | * ... etc 62 | 63 | #### Policy 64 | Required actions: 65 | * `s3:DeleteObject` 66 | * `s3:GetObject` 67 | * `s3:PutObject` 68 | 69 | This is subject to change in future as more features are added. 70 | 71 | ## Building from source 72 | Kipp builds, tests and compiles using [Bazel](https://bazel.build). To run/build 73 | locally with bazel: 74 | ``` 75 | git clone git@github.com:uhthomas/kipp 76 | cd kipp 77 | bazel run //cmd/kipp 78 | ``` 79 | 80 | ## API 81 | Kipp has two main components; uploading files and downloading files. Files can 82 | be uploaded by POSTing a multipart form to the `/` endpoint like so: 83 | ``` 84 | curl https://kipp.6f.io -F file="some content" 85 | ``` 86 | The service will then respond with a `302 (See Other)` status and the location 87 | of the file. It will also write the location to the response body. 88 | 89 | Kipp also serves all files located in the `web` directory by default, but can 90 | either be disabled or changed to a different location. 91 | -------------------------------------------------------------------------------- /WORKSPACE: -------------------------------------------------------------------------------- 1 | # gazelle:repository_macro go_deps.bzl%go_dependencies 2 | workspace(name = "com_github_uhthomas_kipp") 3 | 4 | load("//:deps.bzl", "dependencies") 5 | 6 | dependencies() 7 | 8 | load("@io_bazel_rules_go//go:deps.bzl", "go_register_toolchains", "go_rules_dependencies") 9 | 10 | go_rules_dependencies() 11 | 12 | go_register_toolchains( 13 | go_version = "1.16.3", 14 | nogo = "@//:nogo", 15 | ) 16 | 17 | load("//:go_deps.bzl", "go_dependencies") 18 | 19 | go_dependencies() 20 | 21 | load("@rules_proto//proto:repositories.bzl", "rules_proto_dependencies", "rules_proto_toolchains") 22 | 23 | rules_proto_dependencies() 24 | 25 | rules_proto_toolchains() 26 | 27 | load("@io_bazel_rules_docker//repositories:repositories.bzl", container_repositories = "repositories") 28 | 29 | container_repositories() 30 | 31 | load("@io_bazel_rules_docker//repositories:deps.bzl", container_deps = "deps") 32 | 33 | container_deps() 34 | 35 | load("@io_bazel_rules_docker//go:image.bzl", _go_image_repos = "repositories") 36 | 37 | _go_image_repos() 38 | 39 | load("@bazel_gazelle//:deps.bzl", "gazelle_dependencies") 40 | 41 | gazelle_dependencies() 42 | -------------------------------------------------------------------------------- /cmd/kipp/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@io_bazel_rules_go//go:def.bzl", "go_library") 2 | 3 | go_library( 4 | name = "go_default_library", 5 | srcs = [ 6 | "flag.go", 7 | "main.go", 8 | "mime.go", 9 | "serve.go", 10 | ], 11 | importpath = "github.com/uhthomas/kipp/cmd/kipp", 12 | visibility = ["//visibility:private"], 13 | deps = [ 14 | "//:go_default_library", 15 | "//internal/httputil:go_default_library", 16 | "@com_github_alecthomas_units//:go_default_library", 17 | "@com_github_jackc_pgx_v4//stdlib:go_default_library", 18 | ], 19 | ) 20 | 21 | load("@io_bazel_rules_docker//go:image.bzl", "go_image") 22 | 23 | go_image( 24 | name = "kipp", 25 | data = ["//:web"], 26 | embed = [":go_default_library"], 27 | visibility = ["//visibility:private"], 28 | ) 29 | 30 | load("@io_bazel_rules_docker//docker:docker.bzl", "docker_bundle") 31 | 32 | docker_bundle( 33 | name = "bundle", 34 | images = { 35 | "index.docker.io/uhthomas/kipp:latest": ":kipp", 36 | "index.docker.io/uhthomas/kipp:{STABLE_GIT_REF}": ":kipp", 37 | "ghcr.io/uhthomas/kipp:latest": ":kipp", 38 | "ghcr.io/uhthomas/kipp:{STABLE_GIT_REF}": ":kipp", 39 | }, 40 | ) 41 | 42 | load("@io_bazel_rules_docker//contrib:push-all.bzl", "docker_push") 43 | 44 | docker_push( 45 | name = "push", 46 | bundle = "bundle", 47 | # Pushing layers concurrently sometimes fails. 48 | # See GitHub support ticket 885486. 49 | sequential = True, 50 | ) 51 | -------------------------------------------------------------------------------- /cmd/kipp/flag.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | 6 | "github.com/alecthomas/units" 7 | ) 8 | 9 | type bytesValue units.Base2Bytes 10 | 11 | func newBytesValue(val units.Base2Bytes, p *units.Base2Bytes) *bytesValue { 12 | *p = val 13 | return (*bytesValue)(p) 14 | } 15 | 16 | func flagBytesValue(name string, value units.Base2Bytes, usage string) *units.Base2Bytes { 17 | p := new(units.Base2Bytes) 18 | flag.CommandLine.Var(newBytesValue(value, p), name, usage) 19 | return p 20 | } 21 | 22 | func (d *bytesValue) Set(s string) (err error) { 23 | *(*units.Base2Bytes)(d), err = units.ParseBase2Bytes(s) 24 | return err 25 | } 26 | 27 | func (d *bytesValue) Get() interface{} { return units.Base2Bytes(*d) } 28 | 29 | func (d *bytesValue) String() string { return (*units.Base2Bytes)(d).String() } 30 | -------------------------------------------------------------------------------- /cmd/kipp/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "os" 8 | "os/signal" 9 | "syscall" 10 | ) 11 | 12 | // Main makes writing programs easier by taking a context, and returning an 13 | // error. It gives a more natural way to write mains. 14 | func Main(ctx context.Context) error { 15 | var cmd string 16 | if len(os.Args) > 1 { 17 | cmd = os.Args[1] 18 | } 19 | 20 | if len(cmd) > 0 && cmd[0] == '-' { 21 | cmd = "" 22 | } 23 | 24 | switch cmd { 25 | case "", "serve": 26 | return serve(ctx) 27 | default: 28 | fmt.Printf("unknown command: %s\n", cmd) 29 | return nil 30 | } 31 | } 32 | 33 | func main() { 34 | ctx, cancel := signal.NotifyContext(context.Background(), 35 | os.Interrupt, 36 | syscall.SIGTERM, 37 | ) 38 | defer cancel() 39 | 40 | if err := Main(ctx); err != nil { 41 | log.Fatal(err) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /cmd/kipp/serve.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "fmt" 7 | "log" 8 | "mime" 9 | "time" 10 | 11 | _ "github.com/jackc/pgx/v4/stdlib" 12 | "github.com/uhthomas/kipp" 13 | "github.com/uhthomas/kipp/internal/httputil" 14 | ) 15 | 16 | func serve(ctx context.Context) error { 17 | addr := flag.String("addr", ":80", "listen addr") 18 | db := flag.String("database", "badger", "database - see docs for more information") 19 | fs := flag.String("filesystem", "files", "filesystem - see docs for more information") 20 | web := flag.String("web", "web", "web directory") 21 | limit := flagBytesValue("limit", 150<<20, "upload limit") 22 | lifetime := flag.Duration("lifetime", 24*time.Hour, "file lifetime") 23 | // a negative grace period waits indefinitely 24 | // a zero grace period immediately terminates 25 | gracePeriod := flag.Duration("grace-period", time.Minute, "termination grace period") 26 | flag.Parse() 27 | 28 | for k, v := range mimeTypes { 29 | for _, vv := range v { 30 | if err := mime.AddExtensionType(vv, k); err != nil { 31 | return fmt.Errorf("add mime extension type: %w", err) 32 | } 33 | } 34 | } 35 | 36 | s, err := kipp.New(ctx, 37 | kipp.ParseDB(*db), 38 | kipp.ParseFS(*fs), 39 | kipp.Lifetime(*lifetime), 40 | kipp.Limit(int64(*limit)), 41 | kipp.Data(*web), 42 | ) 43 | if err != nil { 44 | return err 45 | } 46 | 47 | log.Printf("listening on %s", *addr) 48 | return httputil.ListenAndServe(ctx, *addr, s, *gracePeriod) 49 | } 50 | -------------------------------------------------------------------------------- /database/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@io_bazel_rules_go//go:def.bzl", "go_library") 2 | 3 | go_library( 4 | name = "go_default_library", 5 | srcs = ["database.go"], 6 | importpath = "github.com/uhthomas/kipp/database", 7 | visibility = ["//visibility:public"], 8 | ) 9 | -------------------------------------------------------------------------------- /database/badger/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@io_bazel_rules_go//go:def.bzl", "go_library") 2 | 3 | go_library( 4 | name = "go_default_library", 5 | srcs = ["badger.go"], 6 | importpath = "github.com/uhthomas/kipp/database/badger", 7 | visibility = ["//visibility:public"], 8 | deps = [ 9 | "//database:go_default_library", 10 | "@com_github_dgraph_io_badger_v2//:go_default_library", 11 | ], 12 | ) 13 | -------------------------------------------------------------------------------- /database/badger/badger.go: -------------------------------------------------------------------------------- 1 | package badger 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/gob" 7 | "errors" 8 | "fmt" 9 | 10 | "github.com/dgraph-io/badger/v2" 11 | "github.com/uhthomas/kipp/database" 12 | ) 13 | 14 | // Database is a wrapper around a badger database, providing high level 15 | // functions to act a kipp entry database. 16 | type Database struct{ db *badger.DB } 17 | 18 | // Open opens a new badger database. 19 | func Open(name string) (*Database, error) { 20 | db, err := badger.Open(badger.DefaultOptions(name).WithLogger(nil)) 21 | if err != nil { 22 | return nil, fmt.Errorf("open: %w", err) 23 | } 24 | return &Database{db: db}, nil 25 | } 26 | 27 | // Create sets the key, slug with the gob encoded value of e. 28 | func (db *Database) Create(_ context.Context, e database.Entry) error { 29 | var buf bytes.Buffer 30 | if err := gob.NewEncoder(&buf).Encode(e); err != nil { 31 | return fmt.Errorf("gob encode: %w", err) 32 | } 33 | return db.db.Update(func(txn *badger.Txn) error { 34 | return txn.Set([]byte(e.Slug), buf.Bytes()) 35 | }) 36 | } 37 | 38 | // Remove removes the key with the given slug. 39 | func (db *Database) Remove(_ context.Context, slug string) error { 40 | return db.db.Update(func(txn *badger.Txn) error { 41 | return txn.Delete([]byte(slug)) 42 | }) 43 | } 44 | 45 | // Lookup looks up the named entry. 46 | func (db *Database) Lookup(_ context.Context, slug string) (e database.Entry, err error) { 47 | var b []byte 48 | if err := db.db.View(func(txn *badger.Txn) error { 49 | v, err := txn.Get([]byte(slug)) 50 | if err != nil { 51 | return fmt.Errorf("get: %w", err) 52 | } 53 | b, err = v.ValueCopy(b) 54 | return err 55 | }); err != nil { 56 | if errors.Is(err, badger.ErrKeyNotFound) { 57 | return database.Entry{}, database.ErrNoResults 58 | } 59 | return database.Entry{}, fmt.Errorf("view: %w", err) 60 | } 61 | return e, gob.NewDecoder(bytes.NewReader(b)).Decode(&e) 62 | } 63 | 64 | // Ping pings the database. 65 | func (db *Database) Ping(context.Context) error { return nil } 66 | 67 | // Close closes the database. 68 | func (db *Database) Close(context.Context) error { return db.db.Close() } 69 | -------------------------------------------------------------------------------- /database/database.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "time" 7 | ) 8 | 9 | // ErrNoResults is returned when there are no results for the given query. 10 | var ErrNoResults = errors.New("no results") 11 | 12 | // A Database stores and manages data. 13 | type Database interface { 14 | // Create persists the entry to the underlying database, returning 15 | // any errors if present. 16 | Create(ctx context.Context, e Entry) error 17 | // Remove removes the named entry. 18 | Remove(ctx context.Context, slug string) error 19 | // Lookup looks up the named entry. 20 | Lookup(ctx context.Context, slug string) (Entry, error) 21 | // Ping pings the database. 22 | Ping(ctx context.Context) error 23 | // Close closes the database. 24 | Close(ctx context.Context) error 25 | } 26 | 27 | // An Entry stores relevant metadata for files. 28 | type Entry struct { 29 | Slug string 30 | Name string 31 | Sum string 32 | Size int64 33 | Lifetime *time.Time 34 | Timestamp time.Time 35 | } 36 | -------------------------------------------------------------------------------- /database/sql/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@io_bazel_rules_go//go:def.bzl", "go_library") 2 | 3 | go_library( 4 | name = "go_default_library", 5 | srcs = ["sql.go"], 6 | importpath = "github.com/uhthomas/kipp/database/sql", 7 | visibility = ["//visibility:public"], 8 | deps = ["//database:go_default_library"], 9 | ) 10 | -------------------------------------------------------------------------------- /database/sql/sql.go: -------------------------------------------------------------------------------- 1 | package sql 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | "errors" 7 | "fmt" 8 | "time" 9 | 10 | "github.com/uhthomas/kipp/database" 11 | ) 12 | 13 | // A Database is a wrapper around a sql db which provides high level 14 | // functions defined in database.Database. 15 | type Database struct { 16 | db *sql.DB 17 | createStmt *sql.Stmt 18 | removeStmt *sql.Stmt 19 | lookupStmt *sql.Stmt 20 | } 21 | 22 | const initQuery = `CREATE TABLE IF NOT EXISTS entries ( 23 | id SERIAL PRIMARY KEY NOT NULL, 24 | slug VARCHAR(16) NOT NULL, 25 | name VARCHAR(255) NOT NULL, 26 | sum varchar(87) NOT NULL, -- len(b64([64]byte)) 27 | size BIGINT NOT NULL, 28 | lifetime TIMESTAMP, 29 | timestamp TIMESTAMP NOT NULL 30 | ); 31 | 32 | CREATE UNIQUE INDEX IF NOT EXISTS idx_slug ON entries (slug)` 33 | 34 | // Open opens a new sql database and prepares relevant statements. 35 | func Open(ctx context.Context, driver, name string) (_ *Database, err error) { 36 | db, err := sql.Open(driver, name) 37 | if err != nil { 38 | return nil, fmt.Errorf("sql open: %w", err) 39 | } 40 | defer func() { 41 | if err != nil { 42 | db.Close() 43 | } 44 | }() 45 | 46 | db.SetConnMaxIdleTime(5 * time.Minute) 47 | db.SetConnMaxLifetime(5 * time.Minute) 48 | db.SetMaxIdleConns(20) 49 | db.SetMaxOpenConns(25) 50 | 51 | if err := db.PingContext(ctx); err != nil { 52 | return nil, fmt.Errorf("ping: %w", err) 53 | } 54 | if _, err := db.ExecContext(ctx, initQuery); err != nil { 55 | return nil, fmt.Errorf("exec init: %w", err) 56 | } 57 | 58 | d := &Database{db: db} 59 | for _, v := range []struct { 60 | query string 61 | out **sql.Stmt 62 | }{ 63 | {query: createQuery, out: &d.createStmt}, 64 | {query: removeQuery, out: &d.removeStmt}, 65 | {query: lookupQuery, out: &d.lookupStmt}, 66 | } { 67 | var err error 68 | if *v.out, err = db.PrepareContext(ctx, v.query); err != nil { 69 | return nil, fmt.Errorf("prepare: %w", err) 70 | } 71 | } 72 | return d, nil 73 | } 74 | 75 | const createQuery = `INSERT INTO entries ( 76 | slug, 77 | name, 78 | sum, 79 | size, 80 | lifetime, 81 | timestamp 82 | ) VALUES ($1, $2, $3, $4, $5, $6)` 83 | 84 | // Create inserts e into the underlying db. 85 | func (db *Database) Create(ctx context.Context, e database.Entry) error { 86 | if _, err := db.createStmt.ExecContext(ctx, 87 | e.Slug, 88 | e.Name, 89 | e.Sum, 90 | e.Size, 91 | e.Lifetime, 92 | e.Timestamp, 93 | ); err != nil { 94 | return fmt.Errorf("exec: %w", err) 95 | } 96 | return nil 97 | } 98 | 99 | const removeQuery = "DELETE FROM entries WHERE slug = $1" 100 | 101 | // Remove removes the entry with the given slug. 102 | func (db *Database) Remove(ctx context.Context, slug string) error { 103 | if _, err := db.removeStmt.ExecContext(ctx, slug); err != nil { 104 | return fmt.Errorf("exec: %w", err) 105 | } 106 | return nil 107 | } 108 | 109 | const lookupQuery = "SELECT slug, name, sum, size, lifetime, timestamp FROM entries WHERE slug = $1" 110 | 111 | // Lookup looks up the entry for the given slug. 112 | func (db *Database) Lookup(ctx context.Context, slug string) (e database.Entry, err error) { 113 | if err := db.lookupStmt.QueryRowContext(ctx, slug).Scan( 114 | &e.Slug, 115 | &e.Name, 116 | &e.Sum, 117 | &e.Size, 118 | &e.Lifetime, 119 | &e.Timestamp, 120 | ); err != nil { 121 | if errors.Is(err, sql.ErrNoRows) { 122 | return e, database.ErrNoResults 123 | } 124 | return e, fmt.Errorf("query row: %w", err) 125 | } 126 | return e, nil 127 | } 128 | 129 | // Ping pings the underlying db. 130 | func (db *Database) Ping(ctx context.Context) error { return db.db.PingContext(ctx) } 131 | 132 | // Close closes the underlying db. 133 | func (db *Database) Close(context.Context) error { return db.db.Close() } 134 | -------------------------------------------------------------------------------- /deps.bzl: -------------------------------------------------------------------------------- 1 | load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") 2 | 3 | def dependencies(): 4 | http_archive( 5 | name = "bazel_gazelle", 6 | sha256 = "501deb3d5695ab658e82f6f6f549ba681ea3ca2a5fb7911154b5aa45596183fa", 7 | urls = [ 8 | "https://mirror.bazel.build/github.com/bazelbuild/bazel-gazelle/releases/download/v0.26.0/bazel-gazelle-v0.26.0.tar.gz", 9 | "https://github.com/bazelbuild/bazel-gazelle/releases/download/v0.26.0/bazel-gazelle-v0.26.0.tar.gz", 10 | ], 11 | ) 12 | http_archive( 13 | name = "com_github_bazelbuild_buildtools", 14 | sha256 = "e3bb0dc8b0274ea1aca75f1f8c0c835adbe589708ea89bf698069d0790701ea3", 15 | strip_prefix = "buildtools-5.1.0", 16 | url = "https://github.com/bazelbuild/buildtools/archive/5.1.0.tar.gz", 17 | ) 18 | http_archive( 19 | name = "rules_proto", 20 | sha256 = "0728e5414a3aaeb1edd3cf7b68f42920892245ca04176d2db2fd911193155b3d", 21 | strip_prefix = "rules_proto-83da40714cf72a18b4a052d2b421f9668fa4ab69", 22 | urls = ["https://github.com/bazelbuild/rules_proto/archive/83da40714cf72a18b4a052d2b421f9668fa4ab69.tar.gz"], 23 | ) 24 | http_archive( 25 | name = "rules_python", 26 | sha256 = "cd6730ed53a002c56ce4e2f396ba3b3be262fd7cb68339f0377a45e8227fe332", 27 | urls = ["https://github.com/bazelbuild/rules_python/releases/download/0.5.0/rules_python-0.5.0.tar.gz"], 28 | ) 29 | http_archive( 30 | name = "io_bazel_rules_docker", 31 | sha256 = "27d53c1d646fc9537a70427ad7b034734d08a9c38924cc6357cc973fed300820", 32 | strip_prefix = "rules_docker-0.24.0", 33 | urls = ["https://github.com/bazelbuild/rules_docker/releases/download/v0.24.0/rules_docker-v0.24.0.tar.gz"], 34 | ) 35 | http_archive( 36 | name = "io_bazel_rules_go", 37 | sha256 = "7904dbecbaffd068651916dce77ff3437679f9d20e1a7956bff43826e7645fcc", 38 | urls = [ 39 | "https://mirror.bazel.build/github.com/bazelbuild/rules_go/releases/download/v0.25.1/rules_go-v0.25.1.tar.gz", 40 | "https://github.com/bazelbuild/rules_go/releases/download/v0.43.0/rules_go-v0.25.1.tar.gz", 41 | ], 42 | ) 43 | -------------------------------------------------------------------------------- /filesystem/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@io_bazel_rules_go//go:def.bzl", "go_library") 2 | 3 | go_library( 4 | name = "go_default_library", 5 | srcs = ["fs.go"], 6 | importpath = "github.com/uhthomas/kipp/filesystem", 7 | visibility = ["//visibility:public"], 8 | ) 9 | -------------------------------------------------------------------------------- /filesystem/fs.go: -------------------------------------------------------------------------------- 1 | package filesystem 2 | 3 | import ( 4 | "context" 5 | "io" 6 | ) 7 | 8 | // A FileSystem is a persistent store of objects uniquely identified by name. 9 | type FileSystem interface { 10 | // Create creates an object with the specified name, and will read 11 | // from r up to io.EOF. The reader is explicitly passed in to allow 12 | // implementations to cleanup, and guarantee consistency. 13 | Create(ctx context.Context, name string, r io.Reader) error 14 | Open(ctx context.Context, name string) (Reader, error) 15 | Remove(ctx context.Context, name string) error 16 | } 17 | 18 | // A Reader is a readable, seekable and closable file stream. 19 | type Reader interface { 20 | io.ReadSeeker 21 | io.Closer 22 | } 23 | 24 | // PipeReader pipes r to f(w). 25 | func PipeReader(f func(w io.Writer) error) io.Reader { 26 | pr, pw := io.Pipe() 27 | go func() { pw.CloseWithError(f(pw)) }() 28 | return pr 29 | } 30 | -------------------------------------------------------------------------------- /filesystem/local/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") 2 | 3 | go_library( 4 | name = "go_default_library", 5 | srcs = ["local.go"], 6 | importpath = "github.com/uhthomas/kipp/filesystem/local", 7 | visibility = ["//visibility:public"], 8 | deps = ["//filesystem:go_default_library"], 9 | ) 10 | 11 | go_test( 12 | name = "go_default_test", 13 | srcs = ["local_test.go"], 14 | embed = [":go_default_library"], 15 | deps = ["//filesystem:go_default_library"], 16 | ) 17 | -------------------------------------------------------------------------------- /filesystem/local/local.go: -------------------------------------------------------------------------------- 1 | package local 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | "os" 8 | "path/filepath" 9 | 10 | "github.com/uhthomas/kipp/filesystem" 11 | ) 12 | 13 | // A FileSystem contains information about the local filesystem. 14 | type FileSystem struct{ dir, tmp string } 15 | 16 | // New creates a new FileSystem, and makes the relevant directories for 17 | // dir and tmp. 18 | func New(dir string) (*FileSystem, error) { 19 | tmp := filepath.Join(dir, "tmp") 20 | if err := os.MkdirAll(tmp, 0755); err != nil && !os.IsExist(err) { 21 | return nil, err 22 | } 23 | return &FileSystem{dir: dir, tmp: tmp}, nil 24 | } 25 | 26 | // Create writes r to a temporary file, and links it to a permanent location 27 | // upon success. 28 | func (fs FileSystem) Create(_ context.Context, name string, r io.Reader) error { 29 | f, err := os.CreateTemp(fs.tmp, "kipp") 30 | if err != nil { 31 | return fmt.Errorf("temp file: %w", err) 32 | } 33 | defer os.Remove(f.Name()) 34 | defer f.Close() 35 | if _, err := io.Copy(f, r); err != nil { 36 | return fmt.Errorf("copy: %w", err) 37 | } 38 | if err := f.Close(); err != nil { 39 | return fmt.Errorf("close: %w", err) 40 | } 41 | if err := os.Link(f.Name(), fs.path(name)); err != nil && !os.IsExist(err) { 42 | return fmt.Errorf("link: %w", err) 43 | } 44 | return nil 45 | } 46 | 47 | // Open opens the named file. 48 | func (fs FileSystem) Open(_ context.Context, name string) (filesystem.Reader, error) { 49 | return os.Open(fs.path(name)) 50 | } 51 | 52 | // Remove removes the named file. 53 | func (fs FileSystem) Remove(_ context.Context, name string) error { 54 | return os.Remove(fs.path(name)) 55 | } 56 | 57 | // path returns the path of the named file relative to the file system. 58 | func (fs FileSystem) path(name string) string { return filepath.Join(fs.dir, name) } 59 | -------------------------------------------------------------------------------- /filesystem/local/local_test.go: -------------------------------------------------------------------------------- 1 | package local_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/uhthomas/kipp/filesystem" 7 | "github.com/uhthomas/kipp/filesystem/local" 8 | ) 9 | 10 | func TestFileSystem(t *testing.T) { 11 | var i interface{} = (*local.FileSystem)(nil) 12 | if _, ok := i.(filesystem.FileSystem); !ok { 13 | t.Fatal("local.FileSystem does not implement fs.FileSystem") 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /filesystem/s3/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@io_bazel_rules_go//go:def.bzl", "go_library") 2 | 3 | go_library( 4 | name = "go_default_library", 5 | srcs = [ 6 | "reader.go", 7 | "s3.go", 8 | ], 9 | importpath = "github.com/uhthomas/kipp/filesystem/s3", 10 | visibility = ["//visibility:public"], 11 | deps = [ 12 | "//filesystem:go_default_library", 13 | "@com_github_aws_aws_sdk_go//aws:go_default_library", 14 | "@com_github_aws_aws_sdk_go//aws/session:go_default_library", 15 | "@com_github_aws_aws_sdk_go//service/s3:go_default_library", 16 | "@com_github_aws_aws_sdk_go//service/s3/s3manager:go_default_library", 17 | ], 18 | ) 19 | -------------------------------------------------------------------------------- /filesystem/s3/reader.go: -------------------------------------------------------------------------------- 1 | package s3 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "io" 8 | 9 | "github.com/aws/aws-sdk-go/aws" 10 | "github.com/aws/aws-sdk-go/service/s3" 11 | ) 12 | 13 | type reader struct { 14 | ctx context.Context 15 | client *s3.S3 16 | bucket, name string 17 | obj *s3.GetObjectOutput 18 | offset, size int64 19 | } 20 | 21 | func newReader(ctx context.Context, client *s3.S3, bucket, name string) *reader { 22 | return &reader{ 23 | ctx: ctx, 24 | client: client, 25 | bucket: bucket, 26 | name: name, 27 | } 28 | } 29 | 30 | func (r *reader) Read(p []byte) (n int, err error) { 31 | if r.obj != nil { 32 | return r.obj.Body.Read(p) 33 | } 34 | if err := r.reset(); err != nil { 35 | return 0, fmt.Errorf("reset: %w", err) 36 | } 37 | return r.Read(p) 38 | } 39 | 40 | func (r *reader) Seek(offset int64, whence int) (int64, error) { 41 | switch whence { 42 | case io.SeekStart: 43 | case io.SeekCurrent: 44 | offset += r.offset 45 | case io.SeekEnd: 46 | offset = r.size - offset 47 | default: 48 | return 0, fmt.Errorf("invalid whence: %d", whence) 49 | } 50 | if offset < 0 { 51 | return 0, errors.New("invalid offset") 52 | } 53 | r.Close() 54 | r.obj, r.offset = nil, offset 55 | return offset, nil 56 | } 57 | 58 | func (r *reader) Close() error { 59 | if r.obj == nil { 60 | return nil 61 | } 62 | return r.obj.Body.Close() 63 | } 64 | 65 | func (r *reader) reset() error { 66 | r.Close() 67 | in := &s3.GetObjectInput{Bucket: &r.bucket, Key: &r.name} 68 | if r.offset > 0 { 69 | in.Range = aws.String(fmt.Sprintf("bytes=%d-", r.offset)) 70 | } 71 | obj, err := r.client.GetObjectWithContext(r.ctx, in) 72 | if err != nil { 73 | return fmt.Errorf("get object: %w", err) 74 | } 75 | r.obj = obj 76 | if r.size == 0 { 77 | r.size = *obj.ContentLength 78 | } 79 | return nil 80 | } 81 | -------------------------------------------------------------------------------- /filesystem/s3/s3.go: -------------------------------------------------------------------------------- 1 | package s3 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | 8 | "github.com/aws/aws-sdk-go/aws" 9 | "github.com/aws/aws-sdk-go/aws/session" 10 | "github.com/aws/aws-sdk-go/service/s3" 11 | "github.com/aws/aws-sdk-go/service/s3/s3manager" 12 | "github.com/uhthomas/kipp/filesystem" 13 | ) 14 | 15 | // FileSystem is an abstraction over an s3 bucket which allows for the creation, 16 | // opening and removal of objects. 17 | type FileSystem struct { 18 | client *s3.S3 19 | uploader *s3manager.Uploader 20 | bucket string 21 | } 22 | 23 | // New creates a new aws session and s3 client. 24 | func New(bucket string, config *aws.Config) (*FileSystem, error) { 25 | sess, err := session.NewSession(config) 26 | if err != nil { 27 | return nil, fmt.Errorf("new session: %w", err) 28 | } 29 | c := s3.New(sess) 30 | return &FileSystem{ 31 | client: c, 32 | uploader: s3manager.NewUploaderWithClient(c), 33 | bucket: bucket, 34 | }, nil 35 | } 36 | 37 | // Create writes r to the named s3 bucket/object. 38 | func (fs *FileSystem) Create(ctx context.Context, name string, r io.Reader) error { 39 | if _, err := fs.uploader.UploadWithContext(ctx, &s3manager.UploadInput{ 40 | Body: r, 41 | Bucket: aws.String(fs.bucket), 42 | Key: &name, 43 | }); err != nil { 44 | return fmt.Errorf("upload: %w", err) 45 | } 46 | return nil 47 | } 48 | 49 | // Open gets the object with the specified key, name. 50 | func (fs *FileSystem) Open(ctx context.Context, name string) (filesystem.Reader, error) { 51 | r := newReader(ctx, fs.client, fs.bucket, name) 52 | return r, nil 53 | } 54 | 55 | // Remove removes the s3 object specified with key, name, from the bucket. 56 | func (fs *FileSystem) Remove(ctx context.Context, name string) error { 57 | if _, err := fs.client.DeleteObjectWithContext(ctx, &s3.DeleteObjectInput{ 58 | Bucket: &fs.bucket, 59 | Key: &name, 60 | }); err != nil { 61 | return fmt.Errorf("delete object %s/%s: %w", fs.bucket, name, err) 62 | } 63 | return nil 64 | } 65 | -------------------------------------------------------------------------------- /fs.go: -------------------------------------------------------------------------------- 1 | package kipp 2 | 3 | import ( 4 | "net/http" 5 | "os" 6 | "time" 7 | 8 | "github.com/uhthomas/kipp/database" 9 | "github.com/uhthomas/kipp/filesystem" 10 | ) 11 | 12 | // fileSystemFunc implements http.FileSystem. 13 | type fileSystemFunc func(string) (http.File, error) 14 | 15 | func (f fileSystemFunc) Open(name string) (http.File, error) { return f(name) } 16 | 17 | type file struct { 18 | filesystem.Reader 19 | entry database.Entry 20 | } 21 | 22 | func (f *file) Readdir(int) ([]os.FileInfo, error) { return nil, nil } 23 | 24 | func (f *file) Stat() (os.FileInfo, error) { return &fileInfo{entry: f.entry}, nil } 25 | 26 | type fileInfo struct{ entry database.Entry } 27 | 28 | func (fi *fileInfo) Name() string { return fi.entry.Name } 29 | 30 | func (fi *fileInfo) Size() int64 { return fi.entry.Size } 31 | 32 | func (fi *fileInfo) Mode() os.FileMode { return 0600 } 33 | 34 | func (fi *fileInfo) IsDir() bool { return false } 35 | 36 | func (fi *fileInfo) Sys() interface{} { return nil } 37 | 38 | func (fi *fileInfo) ModTime() time.Time { return fi.entry.Timestamp } 39 | -------------------------------------------------------------------------------- /fs_test.go: -------------------------------------------------------------------------------- 1 | package kipp 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os" 7 | "testing" 8 | "time" 9 | 10 | "github.com/uhthomas/kipp/database" 11 | ) 12 | 13 | type fakeFileSystemReader struct{ limit, off int64 } 14 | 15 | func (r fakeFileSystemReader) Read(b []byte) (n int, err error) { 16 | if r.limit == r.off { 17 | return 0, io.EOF 18 | } 19 | if l := r.limit - r.off; int64(len(b)) >= l { 20 | b = b[:l] 21 | } 22 | for i := 0; i < len(b); i++ { 23 | b[i] = 0 24 | } 25 | r.off += int64(len(b)) 26 | return len(b), nil 27 | } 28 | 29 | func (r fakeFileSystemReader) Seek(offset int64, whence int) (n int64, err error) { 30 | switch whence { 31 | case io.SeekStart: 32 | case io.SeekCurrent: 33 | offset += r.off 34 | case io.SeekEnd: 35 | offset += r.limit 36 | default: 37 | return 0, fmt.Errorf("invalid whence: %d", whence) 38 | } 39 | r.off = offset 40 | return offset, nil 41 | } 42 | 43 | func (fakeFileSystemReader) Close() error { return nil } 44 | 45 | func TestFileReaddir(t *testing.T) { 46 | files, err := (&file{}).Readdir(-1) 47 | if err != nil { 48 | t.Fatal(err) 49 | } 50 | if l := len(files); l > 0 { 51 | t.Fatalf("unexpected number of files; got %d, want 0", l) 52 | } 53 | } 54 | 55 | func TestFileStat(t *testing.T) { 56 | e := database.Entry{ 57 | Slug: "some slug", 58 | Name: "some name", 59 | Sum: "some sum", 60 | Size: 123456, 61 | Lifetime: nil, 62 | Timestamp: time.Unix(1234, 5678), 63 | } 64 | fi, err := (&file{entry: e}).Stat() 65 | if err != nil { 66 | t.Fatal(err) 67 | } 68 | if got, want := fi.(*fileInfo).entry, e; got != want { 69 | t.Fatalf("entries are not equal; got %#v, want %#v", got, want) 70 | } 71 | } 72 | 73 | func TestFileInfo(t *testing.T) { 74 | e := database.Entry{ 75 | Name: "some name", 76 | Size: 123456, 77 | Timestamp: time.Unix(1234, 5678), 78 | } 79 | fi := &fileInfo{entry: e} 80 | if got, want := fi.Name(), e.Name; got != want { 81 | t.Fatalf("unexpected name; got %s, want %s", got, want) 82 | } 83 | if got, want := fi.Size(), e.Size; got != want { 84 | t.Fatalf("unexpected size; got %d, want %d", got, want) 85 | } 86 | if got, want := fi.Mode(), os.FileMode(0600); got != want { 87 | t.Fatalf("unexpected mode; got %d, want %d", got, want) 88 | } 89 | if fi.IsDir() { 90 | t.Fatalf("file info reports it's a directory when it shouldn't") 91 | } 92 | if fi.Sys() != nil { 93 | t.Fatal("sys is not nil") 94 | } 95 | if got, want := fi.ModTime(), e.Timestamp; got != want { 96 | t.Fatalf("unexpected mod time; got %s, want %s", got, want) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/uhthomas/kipp 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/alecthomas/units v0.0.0-20231202071711-9a357b53e9c9 7 | github.com/aws/aws-sdk-go v1.48.16 8 | github.com/dgraph-io/badger/v2 v2.2007.4 9 | github.com/gabriel-vasile/mimetype v1.4.3 10 | github.com/jackc/pgx/v4 v4.18.1 11 | github.com/prometheus/client_golang v1.17.0 12 | github.com/zeebo/blake3 v0.2.3 13 | golang.org/x/sync v0.5.0 14 | ) 15 | 16 | require ( 17 | github.com/beorn7/perks v1.0.1 // indirect 18 | github.com/cespare/xxhash v1.1.0 // indirect 19 | github.com/cespare/xxhash/v2 v2.2.0 // indirect 20 | github.com/dgraph-io/ristretto v0.0.3-0.20200630154024-f66de99634de // indirect 21 | github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 // indirect 22 | github.com/dustin/go-humanize v1.0.0 // indirect 23 | github.com/golang/protobuf v1.5.3 // indirect 24 | github.com/golang/snappy v0.0.3 // indirect 25 | github.com/jackc/chunkreader/v2 v2.0.1 // indirect 26 | github.com/jackc/pgconn v1.14.0 // indirect 27 | github.com/jackc/pgio v1.0.0 // indirect 28 | github.com/jackc/pgpassfile v1.0.0 // indirect 29 | github.com/jackc/pgproto3/v2 v2.3.2 // indirect 30 | github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect 31 | github.com/jackc/pgtype v1.14.0 // indirect 32 | github.com/jmespath/go-jmespath v0.4.0 // indirect 33 | github.com/klauspost/compress v1.12.3 // indirect 34 | github.com/klauspost/cpuid/v2 v2.0.12 // indirect 35 | github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect 36 | github.com/pkg/errors v0.9.1 // indirect 37 | github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16 // indirect 38 | github.com/prometheus/common v0.44.0 // indirect 39 | github.com/prometheus/procfs v0.11.1 // indirect 40 | golang.org/x/crypto v0.14.0 // indirect 41 | golang.org/x/net v0.17.0 // indirect 42 | golang.org/x/sys v0.13.0 // indirect 43 | golang.org/x/text v0.13.0 // indirect 44 | google.golang.org/protobuf v1.31.0 // indirect 45 | ) 46 | -------------------------------------------------------------------------------- /hack/workspace-status.sh: -------------------------------------------------------------------------------- 1 | cat < 0 { 33 | var cancel context.CancelFunc 34 | ctx, cancel = context.WithTimeout(ctx, gracePeriod) 35 | defer cancel() 36 | } 37 | } 38 | return s.Shutdown(ctx) 39 | }) 40 | 41 | g.Go(func() error { 42 | if err := s.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { 43 | return fmt.Errorf("listen and serve: %w", err) 44 | } 45 | return nil 46 | }) 47 | return g.Wait() 48 | } 49 | -------------------------------------------------------------------------------- /internal/x/context/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@io_bazel_rules_go//go:def.bzl", "go_library") 2 | 3 | go_library( 4 | name = "go_default_library", 5 | srcs = ["context.go"], 6 | importpath = "github.com/uhthomas/kipp/internal/x/context", 7 | visibility = ["//:__subpackages__"], 8 | ) 9 | -------------------------------------------------------------------------------- /internal/x/context/context.go: -------------------------------------------------------------------------------- 1 | package context 2 | 3 | import ( 4 | "context" 5 | "time" 6 | ) 7 | 8 | type detachedContext struct{ context.Context } 9 | 10 | func Detach(ctx context.Context) context.Context { return detachedContext{Context: ctx} } 11 | 12 | func (detachedContext) Deadline() (time.Time, bool) { return time.Time{}, false } 13 | func (detachedContext) Done() <-chan struct{} { return nil } 14 | func (detachedContext) Err() error { return nil } 15 | -------------------------------------------------------------------------------- /nogo_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "structtag": { 3 | "exclude_files": { 4 | "prow/github/client.go": "intentional violation", 5 | "external/": "external tools don't pass vet" 6 | } 7 | }, 8 | "asmdecl": { 9 | "exclude_files": { 10 | "external/": "external tools don't pass vet" 11 | } 12 | }, 13 | "assign": { 14 | "exclude_files": { 15 | "external/": "external tools don't pass vet" 16 | } 17 | }, 18 | "atomic": { 19 | "exclude_files": { 20 | "external/": "external tools don't pass vet" 21 | } 22 | }, 23 | "bools": { 24 | "exclude_files": { 25 | "external/": "external tools don't pass vet" 26 | } 27 | }, 28 | "buildtag": { 29 | "exclude_files": { 30 | "external/": "external tools don't pass vet" 31 | } 32 | }, 33 | "cgocall": { 34 | "exclude_files": { 35 | "external/": "external tools don't pass vet" 36 | } 37 | }, 38 | "composites": { 39 | "exclude_files": { 40 | "external/": "external tools don't pass vet" 41 | } 42 | }, 43 | "copylocks": { 44 | "exclude_files": { 45 | "external/": "external tools don't pass vet" 46 | } 47 | }, 48 | "httpresponse": { 49 | "exclude_files": { 50 | "external/": "external tools don't pass vet" 51 | } 52 | }, 53 | "loopclosure": { 54 | "exclude_files": { 55 | "external/": "external tools don't pass vet" 56 | } 57 | }, 58 | "lostcancel": { 59 | "exclude_files": { 60 | "external/": "external tools don't pass vet" 61 | } 62 | }, 63 | "nilfunc": { 64 | "exclude_files": { 65 | "external/": "external tools don't pass vet" 66 | } 67 | }, 68 | "printf": { 69 | "exclude_files": { 70 | "external/": "external tools don't pass vet" 71 | } 72 | }, 73 | "shift": { 74 | "exclude_files": { 75 | "external/": "external tools don't pass vet" 76 | } 77 | }, 78 | "stdmethods": { 79 | "exclude_files": { 80 | "external/": "external tools don't pass vet" 81 | } 82 | }, 83 | "tests": { 84 | "exclude_files": { 85 | "external/": "external tools don't pass vet" 86 | } 87 | }, 88 | "unreachable": { 89 | "exclude_files": { 90 | "external/": "external tools don't pass vet" 91 | } 92 | }, 93 | "unsafeptr": { 94 | "exclude_files": { 95 | "external/": "external tools don't pass vet", 96 | "rules_go_work": "external sqlite3 fails vet, for some reason does not get a better workDir()" 97 | } 98 | }, 99 | "unusedresult": { 100 | "exclude_files": { 101 | "external/": "external tools don't pass vet" 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /option.go: -------------------------------------------------------------------------------- 1 | package kipp 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/uhthomas/kipp/database" 8 | "github.com/uhthomas/kipp/filesystem" 9 | "github.com/uhthomas/kipp/internal/databaseutil" 10 | "github.com/uhthomas/kipp/internal/filesystemutil" 11 | ) 12 | 13 | type Option func(ctx context.Context, s *Server) error 14 | 15 | func DB(db database.Database) Option { 16 | return func(ctx context.Context, s *Server) error { 17 | s.Database = db 18 | return nil 19 | } 20 | } 21 | 22 | func ParseDB(ss string) Option { 23 | return func(ctx context.Context, s *Server) error { 24 | db, err := databaseutil.Parse(ctx, ss) 25 | if err != nil { 26 | return err 27 | } 28 | return DB(db)(ctx, s) 29 | } 30 | } 31 | 32 | func FS(fs filesystem.FileSystem) Option { 33 | return func(ctx context.Context, s *Server) error { 34 | s.FileSystem = fs 35 | return nil 36 | } 37 | } 38 | 39 | func ParseFS(ss string) Option { 40 | return func(ctx context.Context, s *Server) error { 41 | fs, err := filesystemutil.Parse(ss) 42 | if err != nil { 43 | return err 44 | } 45 | return FS(fs)(ctx, s) 46 | } 47 | } 48 | 49 | func Lifetime(d time.Duration) Option { 50 | return func(ctx context.Context, s *Server) error { 51 | s.Lifetime = d 52 | return nil 53 | } 54 | } 55 | 56 | func Limit(n int64) Option { 57 | return func(ctx context.Context, s *Server) error { 58 | s.Limit = n 59 | return nil 60 | } 61 | } 62 | 63 | func Data(path string) Option { 64 | return func(ctx context.Context, s *Server) error { 65 | s.PublicPath = path 66 | return nil 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base" 4 | ], 5 | "packageRules": [ 6 | { 7 | "updateTypes": ["minor", "patch", "pin", "digest"], 8 | "automerge": true 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /server.go: -------------------------------------------------------------------------------- 1 | package kipp 2 | 3 | import ( 4 | "context" 5 | "crypto/rand" 6 | "encoding/base64" 7 | "errors" 8 | "fmt" 9 | "io" 10 | "log" 11 | "mime" 12 | "mime/multipart" 13 | "net/http" 14 | "net/url" 15 | "os" 16 | "path" 17 | "path/filepath" 18 | "strconv" 19 | "strings" 20 | "time" 21 | 22 | "github.com/gabriel-vasile/mimetype" 23 | "github.com/prometheus/client_golang/prometheus" 24 | "github.com/prometheus/client_golang/prometheus/promhttp" 25 | "github.com/uhthomas/kipp/database" 26 | "github.com/uhthomas/kipp/filesystem" 27 | "github.com/zeebo/blake3" 28 | ) 29 | 30 | // Server acts as the HTTP server and configuration. 31 | type Server struct { 32 | Database database.Database 33 | FileSystem filesystem.FileSystem 34 | Lifetime time.Duration 35 | Limit int64 36 | PublicPath string 37 | metricHandler http.Handler 38 | } 39 | 40 | func New(ctx context.Context, opts ...Option) (*Server, error) { 41 | r := prometheus.NewRegistry() 42 | if err := r.Register(prometheus.NewGoCollector()); err != nil { 43 | return nil, fmt.Errorf("register go collector: %w", err) 44 | } 45 | s := &Server{ 46 | metricHandler: promhttp.InstrumentMetricHandler( 47 | r, promhttp.HandlerFor(r, promhttp.HandlerOpts{}), 48 | ), 49 | } 50 | for _, opt := range opts { 51 | if err := opt(ctx, s); err != nil { 52 | return nil, err 53 | } 54 | } 55 | return s, nil 56 | } 57 | 58 | // ServeHTTP will serve HTTP requests. It first tries to determine if the 59 | // request is for uploading, it then tries to serve static files and then will 60 | // try to serve public files. 61 | func (s Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { 62 | switch r.Method { 63 | case http.MethodGet, http.MethodHead: 64 | case http.MethodPost: 65 | if r.URL.Path == "/" { 66 | s.UploadHandler(w, r) 67 | return 68 | } 69 | fallthrough 70 | default: 71 | allow := "GET, HEAD, OPTIONS" 72 | if r.URL.Path == "/" { 73 | allow = "GET, HEAD, OPTIONS, POST" 74 | } 75 | if r.Method == http.MethodOptions { 76 | w.Header().Set("Access-Control-Allow-Methods", allow) 77 | } else { 78 | w.Header().Set("Allow", allow) 79 | http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) 80 | } 81 | return 82 | } 83 | 84 | switch r.URL.Path { 85 | case "/healthz": 86 | s.Health(w, r) 87 | return 88 | case "/varz": 89 | s.metricHandler.ServeHTTP(w, r) 90 | return 91 | } 92 | 93 | http.FileServer(fileSystemFunc(func(name string) (_ http.File, err error) { 94 | if f, err := http.Dir(s.PublicPath).Open(name); !os.IsNotExist(err) { 95 | d, err := f.Stat() 96 | if err != nil { 97 | return nil, err 98 | } 99 | if !d.IsDir() { 100 | w.Header().Set("Cache-Control", "max-age=31536000") 101 | // nginx style weak Etag 102 | w.Header().Set("Etag", fmt.Sprintf( 103 | `W/"%x-%x"`, 104 | d.ModTime().Unix(), d.Size(), 105 | )) 106 | } 107 | return f, nil 108 | } 109 | 110 | dir, name := path.Split(name) 111 | if dir != "/" { 112 | return nil, os.ErrNotExist 113 | } 114 | 115 | // trim anything after the first "." 116 | if i := strings.Index(name, "."); i > -1 { 117 | name = name[:i] 118 | } 119 | 120 | e, err := s.Database.Lookup(r.Context(), name) 121 | if err != nil { 122 | if errors.Is(err, database.ErrNoResults) { 123 | return nil, os.ErrNotExist 124 | } 125 | return nil, err 126 | } 127 | 128 | cache := "max-age=31536000" // ~ 1 year 129 | if e.Lifetime != nil { 130 | now := time.Now() 131 | if e.Lifetime.Before(now) { 132 | return nil, os.ErrNotExist 133 | } 134 | cache = fmt.Sprintf( 135 | "public, must-revalidate, max-age=%d", 136 | int(e.Lifetime.Sub(now).Seconds()), 137 | ) 138 | } 139 | 140 | f, err := s.FileSystem.Open(r.Context(), e.Slug) 141 | if err != nil { 142 | return nil, err 143 | } 144 | defer func() { 145 | if err != nil { 146 | f.Close() 147 | } 148 | }() 149 | 150 | ctype, err := detectContentType(e.Name, f) 151 | if err != nil { 152 | return nil, fmt.Errorf("detect content type: %w", err) 153 | } 154 | 155 | // catches text/html and text/html; charset=utf-8 156 | const prefix = "text/html" 157 | if strings.HasPrefix(ctype, prefix) { 158 | ctype = "text/plain" + ctype[len(prefix):] 159 | } 160 | 161 | w.Header().Set("Cache-Control", cache) 162 | w.Header().Set("Content-Disposition", fmt.Sprintf( 163 | "filename=%q; filename*=UTF-8''%[1]s", 164 | url.PathEscape(e.Name), 165 | )) 166 | w.Header().Set("Content-Type", ctype) 167 | w.Header().Set("Etag", strconv.Quote(e.Sum)) 168 | if e.Lifetime != nil { 169 | w.Header().Set("Expires", e.Lifetime.Format(http.TimeFormat)) 170 | } 171 | w.Header().Set("X-Content-Type-Options", "nosniff") 172 | return &file{Reader: f, entry: e}, nil 173 | })).ServeHTTP(w, r) 174 | } 175 | 176 | func (s Server) Health(w http.ResponseWriter, r *http.Request) { 177 | ctx, cancel := context.WithTimeout(r.Context(), time.Second) 178 | defer cancel() 179 | 180 | if err := s.Database.Ping(ctx); err != nil { 181 | log.Printf("ping: %v", err) 182 | http.Error(w, err.Error(), http.StatusInternalServerError) 183 | } 184 | } 185 | 186 | // UploadHandler write the contents of the "file" part to a filesystem.Reader, 187 | // persists the entry to the database and writes the location of the file 188 | // to the response. 189 | func (s Server) UploadHandler(w http.ResponseWriter, r *http.Request) { 190 | // Due to the overhead of multipart bodies, the actual limit for files 191 | // is smaller than it should be. It's not really feasible to calculate 192 | // the overhead so this is *good enough* for the time being. 193 | // 194 | // TODO(thomas): is there a better way to limit the size for the 195 | // part, rather than the whole body? 196 | if r.ContentLength > s.Limit { 197 | http.Error(w, http.StatusText(http.StatusRequestEntityTooLarge), http.StatusRequestEntityTooLarge) 198 | return 199 | } 200 | 201 | r.Body = http.MaxBytesReader(w, r.Body, s.Limit) 202 | 203 | mr, err := r.MultipartReader() 204 | if err != nil { 205 | http.Error(w, err.Error(), http.StatusBadRequest) 206 | return 207 | } 208 | 209 | var p *multipart.Part 210 | for { 211 | if p, err = mr.NextPart(); err != nil { 212 | http.Error(w, err.Error(), http.StatusBadRequest) 213 | return 214 | } 215 | if p.FormName() == "file" { 216 | break 217 | } 218 | } 219 | defer p.Close() 220 | 221 | name := p.FileName() 222 | if len(name) > 255 { 223 | http.Error(w, "invalid name", http.StatusBadRequest) 224 | return 225 | } 226 | 227 | var b [9]byte 228 | if _, err := io.ReadFull(rand.Reader, b[:]); err != nil { 229 | http.Error(w, err.Error(), http.StatusInternalServerError) 230 | return 231 | } 232 | 233 | slug := base64.RawURLEncoding.EncodeToString(b[:]) 234 | 235 | if err := s.FileSystem.Create(r.Context(), slug, filesystem.PipeReader(func(w io.Writer) error { 236 | h := blake3.New() 237 | n, err := io.Copy(io.MultiWriter(w, h), p) 238 | if err != nil { 239 | return fmt.Errorf("copy: %w", err) 240 | } 241 | 242 | now := time.Now() 243 | 244 | var l *time.Time 245 | if s.Lifetime > 0 { 246 | t := now.Add(s.Lifetime) 247 | l = &t 248 | } 249 | 250 | if err := s.Database.Create(r.Context(), database.Entry{ 251 | Slug: slug, 252 | Name: name, 253 | Sum: base64.RawURLEncoding.EncodeToString(h.Sum(nil)), 254 | Size: n, 255 | Timestamp: now, 256 | Lifetime: l, 257 | }); err != nil { 258 | return fmt.Errorf("create entry: %w", err) 259 | } 260 | return nil 261 | })); err != nil { 262 | http.Error(w, err.Error(), http.StatusInternalServerError) 263 | return 264 | } 265 | 266 | ext := filepath.Ext(name) 267 | 268 | var sb strings.Builder 269 | sb.Grow(len(slug) + len(ext) + 2) 270 | sb.WriteRune('/') 271 | sb.WriteString(slug) 272 | sb.WriteString(ext) 273 | 274 | http.Redirect(w, r, sb.String(), http.StatusSeeOther) 275 | 276 | sb.WriteRune('\n') 277 | io.WriteString(w, sb.String()) 278 | } 279 | 280 | // detectContentType sniffs up-to the first 3072 bytes of the stream, 281 | // falling back to extension if the content type could not be detected. 282 | func detectContentType(name string, r io.ReadSeeker) (string, error) { 283 | var b [3072]byte 284 | n, _ := io.ReadFull(r, b[:]) 285 | if _, err := r.Seek(0, io.SeekStart); err != nil { 286 | return "", errors.New("seeker can't seek") 287 | } 288 | m := mimetype.Detect(b[:n]) 289 | if m.Is("application/octet-stream") { 290 | if ctype := mime.TypeByExtension(filepath.Ext(name)); ctype != "" { 291 | return ctype, nil 292 | } 293 | } 294 | return m.String(), nil 295 | } 296 | -------------------------------------------------------------------------------- /tools/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@io_bazel_rules_go//go:def.bzl", "go_library") 2 | 3 | go_library( 4 | name = "go_default_library", 5 | srcs = ["tools.go"], 6 | importpath = "github.com/uhthomas/kipp/tools", 7 | visibility = ["//visibility:public"], 8 | ) 9 | -------------------------------------------------------------------------------- /tools/tools.go: -------------------------------------------------------------------------------- 1 | // +build tools 2 | 3 | package tools 4 | -------------------------------------------------------------------------------- /web/css/main.css: -------------------------------------------------------------------------------- 1 | :root{ 2 | --accent: #00e676; 3 | --accent-dark: #00e6768a; 4 | --accent-secondary: #FF1744; 5 | } 6 | 7 | ::-webkit-file-upload-button { 8 | cursor: pointer; 9 | } 10 | 11 | .material-icons { 12 | color: inherit; 13 | } 14 | 15 | a { 16 | text-decoration: none; 17 | -webkit-transition: color 280ms cubic-bezier(0.4, 0, 0.2, 1); 18 | -moz-transition: color 280ms cubic-bezier(0.4, 0, 0.2, 1); 19 | -ms-transition: color 280ms cubic-bezier(0.4, 0, 0.2, 1); 20 | -o-transition: color 280ms cubic-bezier(0.4, 0, 0.2, 1); 21 | transition: color 280ms cubic-bezier(0.4, 0, 0.2, 1); 22 | } 23 | 24 | * { 25 | padding: 0; 26 | margin: 0; 27 | } 28 | 29 | 30 | a[href] { 31 | color: var(--accent); 32 | } 33 | 34 | /* components */ 35 | 36 | .switch { 37 | width: 36px; 38 | height: 14px; 39 | border-radius: 7px; 40 | background: #6d6d6d; 41 | transition: background-position-x 280ms cubic-bezier(0.4, 0, 0.2, 1); 42 | position: relative; 43 | margin-left: auto; 44 | background: linear-gradient(to right, var(--accent-dark) 50%, #6d6d6d 50%); 45 | background-size: 200% 100%; 46 | background-position-x: 100%; 47 | } 48 | 49 | .switch::before { 50 | content: ''; 51 | width: 20px; 52 | height: 20px; 53 | border-radius: 10px; 54 | position: absolute; 55 | top: -3px; 56 | background: #bdc1c6; 57 | box-shadow: 0 3px 1px -2px rgba(0,0,0,.2), 0 2px 2px 0 rgba(0,0,0,.14), 0 1px 5px 0 rgba(0,0,0,.12); 58 | transition: transform 280ms cubic-bezier(0.4, 0, 0.2, 1), background 280ms cubic-bezier(0.4, 0, 0.2, 1); 59 | } 60 | 61 | .switch.on { 62 | background-position-x: 0%; 63 | } 64 | 65 | .switch.on::before { 66 | transform: translate3d(16px, 0, 0); 67 | background: var(--accent); 68 | } 69 | 70 | #fab { 71 | position: absolute; 72 | top: -24px; 73 | background: var(--accent); 74 | color: black; 75 | z-index: 1; 76 | box-shadow: 0 3px 5px -1px rgba(0,0,0,.2), 0 6px 10px 0 rgba(0,0,0,.14), 0 1px 18px 0 rgba(0,0,0,.12); 77 | border-radius: 24px; 78 | cursor: pointer; 79 | overflow: hidden; 80 | } 81 | 82 | #fab::before { 83 | content: 'add'; 84 | font-family: 'Material Icons'; 85 | font-weight: normal; 86 | font-style: normal; 87 | font-size: 24px; 88 | line-height: 1; 89 | letter-spacing: normal; 90 | text-transform: none; 91 | display: inline-block; 92 | white-space: nowrap; 93 | word-wrap: normal; 94 | direction: ltr; 95 | -webkit-font-feature-settings: 'liga'; 96 | -webkit-font-smoothing: antialiased; 97 | 98 | padding: 12px; 99 | } 100 | 101 | #fab::after { 102 | content: 'Add files'; 103 | letter-spacing: 0.046875rem; 104 | padding-right: 20px; 105 | line-height: 48px; 106 | font-weight: 500; 107 | font-size: 14px; 108 | float: right; 109 | } 110 | 111 | #fab input { 112 | opacity: 0; 113 | position: absolute; 114 | top: 0; 115 | left: 0; 116 | cursor: pointer; 117 | width: 0; 118 | height: 0; 119 | z-index: -1; 120 | } 121 | 122 | button { 123 | margin-left: 8px; 124 | min-width: 64px; 125 | text-align: center; 126 | padding: 0 16px; 127 | height: 36px; 128 | line-height: 36px; 129 | box-sizing: border-box; 130 | border-radius: 4px; 131 | font: inherit; 132 | font-weight: 500; 133 | font-size: 14px; 134 | letter-spacing: 0.046875rem; 135 | transition: background 280ms cubic-bezier(0.4, 0, 0.2, 1); 136 | background: none; 137 | border: none; 138 | cursor: pointer; 139 | color: inherit; 140 | outline: none; 141 | } 142 | 143 | button:focus, 144 | button:hover { 145 | background: #ffffff1c; 146 | } 147 | 148 | .dialog { 149 | min-width: 280px; 150 | max-width: 480px; 151 | margin: auto; 152 | overflow: hidden; 153 | background: #313235; 154 | border-radius: 16px; 155 | z-index: 1; 156 | position: fixed; 157 | left: 50%; 158 | top: 50%; 159 | transform: translate(-50%, -50%); 160 | } 161 | 162 | .dialog .title { 163 | font-size: 22px; 164 | padding: 24px 24px 20px 24px; 165 | } 166 | 167 | .dialog .text { 168 | font-size: 16px; 169 | color: rgba(255, 255, 255, 0.7); 170 | box-sizing: border-box; 171 | padding: 0 24px 24px 24px; 172 | } 173 | 174 | .dialog .buttons { 175 | display: flex; 176 | justify-content: flex-end; 177 | padding: 0 12px 12px 12px; 178 | } 179 | 180 | .dialog .buttons .button { 181 | margin: 8px 8px 8px 0; 182 | } 183 | 184 | .dialog .buttons button:last-child { 185 | color: #03A9F4; 186 | } 187 | 188 | .dialog .buttons button:hover { 189 | background: rgba(255, 255, 255, 0.12); 190 | } 191 | 192 | .share { 193 | max-width: 800px; 194 | width: 100%; 195 | padding-bottom: 16px; 196 | box-sizing: border-box; 197 | display: flex; 198 | flex-direction: column; 199 | position: relative; 200 | transform: translateY(calc(100% + 32px)); 201 | transition: transform 280ms cubic-bezier(0.4, 0, 0.2, 1); 202 | margin-top: auto; 203 | margin-bottom: 0; 204 | border-radius: 0; 205 | border-top-left-radius: 16px; 206 | border-top-right-radius: 16px; 207 | box-shadow: 0px 3px 1px -2px rgba(0, 0, 0, 0.2), 0px 2px 2px 0px rgba(0, 0, 0, 0.14), 0px 1px 5px 0px rgba(0, 0, 0, 0.12); 208 | background: #313235; 209 | overflow: hidden; 210 | position: fixed; 211 | bottom: 0; 212 | } 213 | 214 | .share.open { 215 | transform: translateY(0px); 216 | } 217 | 218 | .share .title { 219 | height: 56px; 220 | line-height: 56px; 221 | color: rgba(255, 255, 255, 0.7); 222 | overflow: hidden; 223 | text-overflow: ellipsis; 224 | white-space: nowrap; 225 | box-sizing: border-box; 226 | padding: 0 16px; 227 | } 228 | 229 | .share .extra { 230 | background: #202125; 231 | color: rgba(255, 255, 255, 0.5); 232 | border: 0; 233 | outline: 0; 234 | font: inherit; 235 | cursor: text; 236 | } 237 | 238 | .share .item { 239 | cursor: pointer; 240 | display: flex; 241 | transition: background 280ms cubic-bezier(0.4, 0, 0.2, 1); 242 | } 243 | 244 | .share .item:hover { 245 | background: rgba(255, 255, 255, 0.12); 246 | } 247 | 248 | .share .item .icon { 249 | padding: 12px 16px; 250 | } 251 | 252 | .share .item .text { 253 | padding: 0 16px; 254 | line-height: 48px; 255 | } 256 | 257 | /* main */ 258 | 259 | html, 260 | body { 261 | width: 100%; 262 | height: 100%; 263 | } 264 | 265 | body { 266 | font-family: 'Product Sans', arial, sans-serif; 267 | font-size: 16px; 268 | display: flex; 269 | justify-content: center; 270 | } 271 | 272 | main { 273 | width: 100%; 274 | max-width: 800px; 275 | min-height: 100%; 276 | position: relative; 277 | padding-bottom: 70px; 278 | box-sizing: border-box; 279 | height: fit-content; 280 | } 281 | 282 | main > .header { 283 | height: 56px; 284 | width: 100%; 285 | line-height: 56px; 286 | text-align: center; 287 | font-size: 20px; 288 | font-weight: 500; 289 | flex-shrink: 0; 290 | box-sizing: border-box; 291 | margin-bottom: 16px; 292 | } 293 | 294 | main .empty { 295 | display: flex; 296 | justify-content: center; 297 | align-items: center; 298 | flex-direction: column; 299 | position: absolute; 300 | top: 0; 301 | left: 0; 302 | right: 0; 303 | bottom: 0; 304 | z-index: -1; 305 | transition: opacity 280ms cubic-bezier(0.4, 0, 0.2, 1); 306 | } 307 | 308 | main .file ~ .empty { 309 | opacity: 0; 310 | } 311 | 312 | main .empty .icon { 313 | font-size: 56px; 314 | margin: 18px; 315 | color: var(--accent); 316 | } 317 | 318 | main .empty span { 319 | font-size: 24px; 320 | margin-bottom: 8px; 321 | } 322 | 323 | main .empty p { 324 | font-size: 16px; 325 | color: #ffffff99; 326 | } 327 | 328 | main .file { 329 | display: flex; 330 | position: relative; 331 | overflow: hidden; 332 | flex-direction: column; 333 | background-size: cover; 334 | max-height: 0; 335 | transition-property: max-height, padding, margin-bottom, box-shadow; 336 | transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); 337 | transition-duration: 280ms; 338 | border-radius: 16px; 339 | box-shadow: 0 0 0 1px #ffffff1c; 340 | margin: 0 16px; 341 | } 342 | 343 | main .file[rendered] { 344 | max-height: 172px; 345 | padding: 16px 0; 346 | margin-bottom: 16px; 347 | height: 140px; 348 | } 349 | 350 | main .file:before { 351 | content: ''; 352 | position: absolute; 353 | top: 0; 354 | left: 0; 355 | width: 100%; 356 | height: 100%; 357 | background-size: 800px 172px; 358 | background-position: center; 359 | background-image: var(--background); 360 | opacity: 0; 361 | transition: opacity 280ms cubic-bezier(0.4, 0, 0.2, 1); 362 | z-index: -1; 363 | } 364 | 365 | main .file[style] { 366 | box-shadow: none; 367 | } 368 | 369 | main .file[style]:before { 370 | opacity: 1; 371 | } 372 | 373 | main .file .top { 374 | padding: 0 16px; 375 | display: flex; 376 | margin-bottom: 16px; 377 | } 378 | 379 | main .file .top .info { 380 | display: flex; 381 | flex-direction: column; 382 | width: 100%; 383 | overflow: hidden; 384 | padding-right: 8px; 385 | } 386 | 387 | main .file .top .info .overline { 388 | font-size: 10px; 389 | line-height: 24px; 390 | color: #ffffff99; 391 | letter-spacing: 0.09375rem; 392 | } 393 | 394 | main .file .top .info .headline { 395 | font-size: 24px; 396 | line-height: 40px; 397 | } 398 | 399 | main .file .top .info .text { 400 | font-size: 14px; 401 | line-height: 24px; 402 | color: #ffffff99; 403 | letter-spacing: 0.015625rem; 404 | } 405 | 406 | main .file .top .info .overline, 407 | main .file .top .info .headline, 408 | main .file .top .info .text { 409 | white-space: nowrap; 410 | text-overflow: ellipsis; 411 | overflow: hidden; 412 | } 413 | 414 | main .file .top .image { 415 | height: 40px; 416 | width: 40px; 417 | border-radius: 20px; 418 | background-size: cover; 419 | background-position: center; 420 | border-radius: 8px; 421 | width: 80px; 422 | height: 80px; 423 | flex-shrink: 0; 424 | } 425 | 426 | main .file .buttons { 427 | display: flex; 428 | } 429 | 430 | main .file .buttons button { 431 | display: none; 432 | color: var(--accent); 433 | } 434 | 435 | main .file[state="error"] .buttons button.primary, 436 | main .file[state="uploading"] .buttons button.primary, 437 | main .file[state="done"] .buttons button.secondary, 438 | main .file[state="done"] .buttons button.primary { 439 | display: block; 440 | } 441 | 442 | main .file[state="error"] .buttons button.primary:before { 443 | content: 'Remove'; 444 | } 445 | 446 | main .file[state="archiving"] .buttons button.primary:before, 447 | main .file[state="encrypting"] .buttons button.primary, 448 | main .file[state="uploading"] .buttons button.primary:before { 449 | content: 'Cancel'; 450 | } 451 | 452 | main .file[state="done"] .buttons button.secondary:before { 453 | content: 'Remove'; 454 | color: var(--accent-secondary); 455 | } 456 | 457 | main .file[state="done"] .buttons button.primary:before { 458 | content: 'Share'; 459 | } 460 | 461 | main .darken { 462 | position: fixed; 463 | top: 0; 464 | left: 0; 465 | width: 100vw; 466 | height: 100vh; 467 | transition: background 280ms cubic-bezier(0.4, 0, 0.2, 1); 468 | } 469 | 470 | main .darken.open { 471 | background: rgba(0,0,0,0.2); 472 | } 473 | 474 | main .drawer { 475 | width: 100%; 476 | background: #313235; 477 | border-top-left-radius: 16px; 478 | border-top-right-radius: 16px; 479 | box-shadow: 0 3px 1px -2px rgba(0,0,0,.2), 0 2px 2px 0 rgba(0,0,0,.14), 0 1px 5px 0 rgba(0,0,0,.12); 480 | position: fixed; 481 | transition: transform 280ms cubic-bezier(0.4, 0, 0.2, 1); 482 | display: flex; 483 | flex-direction: column; 484 | align-items: center; 485 | flex-shrink: 0; 486 | transform: translate3d(0, calc(100% - 56px), 0); 487 | box-sizing: border-box; 488 | bottom: 0; 489 | max-width: inherit; 490 | } 491 | 492 | main .drawer.open { 493 | transform: translate3d(0, 0, 0); 494 | } 495 | 496 | main .drawer .header { 497 | line-height: 56px; 498 | box-sizing: border-box; 499 | width: 100%; 500 | display: flex; 501 | letter-spacing: 0.009375rem; 502 | padding: 0 16px; 503 | box-sizing: border-box; 504 | } 505 | 506 | main .drawer .header .icon { 507 | padding: 16px 0 16px 16px; 508 | margin-left: auto; 509 | cursor: pointer; 510 | } 511 | 512 | main .drawer .header::after { 513 | content: 'expand_less'; 514 | font-family: 'Material Icons'; 515 | font-weight: normal; 516 | font-style: normal; 517 | font-size: 24px; 518 | line-height: 1; 519 | letter-spacing: normal; 520 | text-transform: none; 521 | display: inline-block; 522 | white-space: nowrap; 523 | word-wrap: normal; 524 | direction: ltr; 525 | -webkit-font-feature-settings: 'liga'; 526 | -webkit-font-smoothing: antialiased; 527 | 528 | padding: 16px 0 16px 16px; 529 | cursor: pointer; 530 | margin-left: auto; 531 | } 532 | 533 | main .drawer.open .header::after { 534 | content: 'expand_more'; 535 | } 536 | 537 | main .drawer .item { 538 | display: flex; 539 | align-items: center; 540 | min-height: 48px; 541 | width: 100%; 542 | padding: 0 16px; 543 | box-sizing: border-box; 544 | } 545 | 546 | main .drawer .item[icon] { 547 | height: 56px; 548 | line-height: 56px; 549 | } 550 | 551 | main .drawer .item[icon="report"] { 552 | --accent: var(--accent-secondary); 553 | } 554 | 555 | main .drawer .item[icon]::before { 556 | font-family: 'Material Icons'; 557 | font-weight: normal; 558 | font-style: normal; 559 | font-size: 24px; 560 | line-height: 1; 561 | letter-spacing: normal; 562 | text-transform: none; 563 | display: inline-block; 564 | white-space: nowrap; 565 | word-wrap: normal; 566 | direction: ltr; 567 | -webkit-font-feature-settings: 'liga'; 568 | -webkit-font-smoothing: antialiased; 569 | 570 | content: attr(icon); 571 | float: left; 572 | display: block; 573 | padding: 16px 16px 16px 0; 574 | color: var(--accent); 575 | } 576 | 577 | main .drawer .item.toggle { 578 | cursor: pointer; 579 | } 580 | 581 | main .drawer .curl { 582 | margin: 16px; 583 | padding: 16px 14px; 584 | border: 1px solid #ffffff1e; 585 | border-radius: 26.5px; 586 | color: inherit; 587 | font-size: inherit; 588 | font-family: monospace; 589 | background: none; 590 | width: calc(100% - 32px); 591 | box-sizing: border-box; 592 | } 593 | -------------------------------------------------------------------------------- /web/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uhthomas/kipp/86e9763195c6865722b423f12eeaf3bb880725d1/web/favicon-16x16.png -------------------------------------------------------------------------------- /web/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uhthomas/kipp/86e9763195c6865722b423f12eeaf3bb880725d1/web/favicon-32x32.png -------------------------------------------------------------------------------- /web/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uhthomas/kipp/86e9763195c6865722b423f12eeaf3bb880725d1/web/favicon.ico -------------------------------------------------------------------------------- /web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Kipp 8 | 9 | 10 | 11 | 12 | 13 | 19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 |
Kipp
27 |
28 | folder_open 29 | Nothing yet 30 |

Add some files to get started

31 |
32 |
33 |
34 |
35 |
The easy to use, open source, secure temporary file storage server.
36 |
Encrypt files with AES128-GCM
37 |
150MB max file size
38 |
Files will expire after 24 hours
39 |
Report abuse to abuse@6f.io
40 | 41 |
42 |
43 | 59 | 66 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /web/js/filesize.min.js: -------------------------------------------------------------------------------- 1 | /* 2 | 2015 Jason Mulligan 3 | @version 3.1.2 4 | */ 5 | "use strict";!function(a){var b=/b$/,c={bits:["B","kb","Mb","Gb","Tb","Pb","Eb","Zb","Yb"],bytes:["B","kB","MB","GB","TB","PB","EB","ZB","YB"]},d=function(a){var d=void 0===arguments[1]?{}:arguments[1],e=[],f=!1,g=0,h=void 0,i=void 0,j=void 0,k=void 0,l=void 0,m=void 0,n=void 0,o=void 0,p=void 0,q=void 0,r=void 0;if(isNaN(a))throw new Error("Invalid arguments");return j=d.bits===!0,p=d.unix===!0,i=void 0!==d.base?d.base:2,o=void 0!==d.round?d.round:p?1:2,q=void 0!==d.spacer?d.spacer:p?"":" ",r=void 0!==d.suffixes?d.suffixes:{},n=void 0!==d.output?d.output:"string",h=void 0!==d.exponent?d.exponent:-1,m=Number(a),l=0>m,k=i>2?1e3:1024,l&&(m=-m),0===m?(e[0]=0,e[1]=p?"":"B"):((-1===h||isNaN(h))&&(h=Math.floor(Math.log(m)/Math.log(k))),h>8&&(g=1e3*g*(h-8),h=8),g=2===i?m/Math.pow(2,10*h):m/Math.pow(1e3,h),j&&(g=8*g,g>k&&(g/=k,h++)),e[0]=Number(g.toFixed(h>0?o:0)),e[1]=c[j?"bits":"bytes"][h],!f&&p&&(j&&b.test(e[1])&&(e[1]=e[1].toLowerCase()),e[1]=e[1].charAt(0),"B"===e[1]?(e[0]=Math.floor(e[0]),e[1]=""):j||"k"!==e[1]||(e[1]="K"))),l&&(e[0]=-e[0]),e[1]=r[e[1]]||e[1],"array"===n?e:"exponent"===n?h:"object"===n?{value:e[0],suffix:e[1]}:e.join(q)};"undefined"!=typeof exports?module.exports=d:"function"==typeof define?define(function(){return d}):a.filesize=d}("undefined"!=typeof global?global:window); 6 | //# sourceMappingURL=filesize.min.js.map -------------------------------------------------------------------------------- /web/js/main.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | const pica = window.pica(); 3 | const qr = new QRious({ 4 | size: 256, 5 | backgroundAlpha: 0, 6 | foreground: 'white' 7 | }); 8 | 9 | const encode = arr => btoa(String.fromCharCode.apply(null, arr)).slice(0, -2).replace(/\+/g, '-').replace(/\//g, '_') 10 | 11 | async function encrypt(data) { 12 | const iv = crypto.getRandomValues(new Uint8Array(12)); 13 | const key = await crypto.subtle.generateKey({ name: 'AES-GCM', length: 128 }, true, ['encrypt']); 14 | return [ 15 | Array.from(iv), 16 | Array.from(new Uint8Array(await crypto.subtle.exportKey('raw', key))), 17 | new Blob([new Uint8Array(await crypto.subtle.encrypt({ name: 'AES-GCM', iv: iv }, key, data))]) 18 | ]; 19 | } 20 | 21 | const importTemplate = template => { 22 | const d = document.createElement('div'); 23 | d.appendChild(document.importNode(template.content, true)); 24 | return d.children[0]; 25 | } 26 | 27 | // main and template are bound 28 | const dialog = ((main, template, title, content, buttons) => { 29 | const el = importTemplate(template); 30 | const undarken = darken(() => el.remove()); 31 | el.addEventListener('click', e => e.target.tagName === 'BUTTON' && undarken()); 32 | el.querySelector('.title').innerText = title; 33 | if (typeof content === 'function') content(el.querySelector('.text')); 34 | else el.querySelector('.text').innerHTML = content; 35 | const elb = el.querySelector('.buttons'); 36 | for (var i = 0; i < buttons.length; i++) { 37 | const b = document.createElement('button'); 38 | b.innerText = buttons[i].text; 39 | if (buttons[i].f) b.addEventListener('click', buttons[i].f); 40 | elb.appendChild(b); 41 | } 42 | main.appendChild(el); 43 | }).bind(this, document.getElementsByTagName('main')[0], document.getElementById('dialog-template')); 44 | 45 | const [darken, undarken] = (() => { 46 | const main = document.getElementsByTagName('main')[0]; 47 | const drawer = document.querySelector('.drawer'); 48 | var d, callback; 49 | const darken = (c, isDrawer) => { 50 | if (!d) { 51 | d = document.createElement('div'); 52 | d.className = 'darken'; 53 | d.onclick = e => e.target == d && undarken(); 54 | requestAnimationFrame(() => d.classList.add('open')); 55 | } else undarken(true); 56 | 57 | d.remove(); 58 | if (isDrawer) main.insertBefore(d, drawer); 59 | else main.appendChild(d); 60 | 61 | callback = c 62 | 63 | return () => callback === c && undarken(); 64 | } 65 | 66 | const undarken = reuse => { 67 | if (callback) callback(); 68 | if (!d || reuse) return; 69 | d.addEventListener('transitionend', d.remove); 70 | d.classList.remove('open'); 71 | d = null; 72 | } 73 | return [darken, undarken]; 74 | })() 75 | 76 | document.getElementById('fab').onclick = (a => a.click()).bind(this, document.querySelector('#fab input')); 77 | 78 | document.querySelector('main .drawer .header').onclick = (drawer => { 79 | if (drawer.classList.contains('open')) return undarken(); 80 | darken(() => drawer.classList.remove('open'), true); 81 | drawer.classList.add('open'); 82 | }).bind(this, document.querySelector('main .drawer')); 83 | 84 | var encryption = localStorage.getItem('encryption') === 'true'; 85 | document.querySelector('main .drawer .item.encryption').onclick = (s => { 86 | (encryption = !encryption) ? s.classList.add('on') : s.classList.remove('on'); 87 | localStorage.setItem('encryption', '' + encryption); 88 | }).bind(this, document.querySelector('main .drawer .item.encryption .switch')); 89 | if (encryption) document.querySelector('main .drawer .item.encryption .switch').classList.add('on'); 90 | 91 | const fileElements = []; 92 | function FileElement(blob, name) { 93 | const self = this; 94 | 95 | self.encryption = encryption; 96 | 97 | // setImage will try to determine what the blob is and then render a 98 | // preview image for the FileElement. 99 | self.setImage = async blob => { 100 | const u = URL.createObjectURL(blob); 101 | 102 | const video = async () => { 103 | const video = await new Promise((resolve, reject) => { 104 | const video = document.createElement('video'); 105 | video.onloadeddata = () => resolve(video); 106 | video.onerror = reject; 107 | video.src = u; 108 | }); 109 | const canvas = document.createElement('canvas'); 110 | canvas.width = video.videoWidth; 111 | canvas.height = video.videoHeight; 112 | canvas.getContext('2d').drawImage(video, 0, 0); 113 | await self.setImage(await pica.toBlob(canvas, 'image/png', 1)); 114 | } 115 | 116 | const audio = async () => { 117 | await self.setImage(await new Promise((resolve, reject) => { 118 | musicmetadata(blob, (err, info) => { 119 | if (err || info.picture.length < 1) return reject(err || new Error('No album art')); 120 | const image = info.picture[0]; 121 | resolve(new Blob([image.data], { 122 | type: 'image/' + image.format 123 | })); 124 | }); 125 | })); 126 | } 127 | 128 | const image = async () => { 129 | const img = await new Promise((resolve, reject) => { 130 | const img = new Image(); 131 | img.onload = () => resolve(img); 132 | img.onerror = reject; 133 | img.src = u; 134 | }); 135 | 136 | const r = 800 / 172; 137 | const nr = img.naturalWidth / img.naturalHeight; 138 | 139 | // First large blurred background. 140 | const src = document.createElement('canvas'); 141 | src.height = img.naturalHeight; 142 | src.width = img.naturalWidth; 143 | if (nr > r) src.width = src.height * r; 144 | else if (nr < r) src.height = src.width / r; 145 | src.getContext('2d').drawImage(img, (src.width - img.naturalWidth) / 2, (src.height - img.naturalHeight) / 2); 146 | 147 | const dst = document.createElement('canvas'); 148 | dst.width = 800; 149 | dst.height = 172; 150 | 151 | await pica.resize(src, dst, { alpha: true }); 152 | 153 | // Darken background before rendering as blob 154 | const ctx = dst.getContext('2d'); 155 | ctx.fillStyle = '#00000080'; 156 | ctx.fillRect(0, 0, dst.width, dst.height); 157 | StackBlur.canvasRGBA(dst, 0, 0, dst.width, dst.height, 20); 158 | 159 | const blob = await pica.toBlob(dst, 'image/png', 1); 160 | 161 | // Second small 'avatar' preview. 162 | src.width = src.height = Math.min(img.naturalWidth, img.naturalHeight); 163 | src.getContext('2d').drawImage(img, (src.width - img.naturalWidth) / 2, (src.height - img.naturalHeight) / 2); 164 | 165 | dst.width = dst.height = 80; 166 | 167 | await pica.resize(src, dst, { alpha: true }); 168 | 169 | const blob2 = await pica.toBlob(dst, 'image/png', 1); 170 | self.element.setAttribute('style', '--background: url(' + URL.createObjectURL(blob) + ')'); 171 | self.element.querySelector('.image').style.backgroundImage = 'url(' + URL.createObjectURL(blob2) + ')'; 172 | } 173 | 174 | const none = async () => { throw new Error('Not an image') }; 175 | 176 | try { 177 | await ({ 'video': video, 'audio': audio, 'image': image }[blob.type.split('/')[0]] || none)(); 178 | } catch (e) { throw e; } finally { URL.revokeObjectURL(u); } 179 | } 180 | 181 | // setBlob will set the underlying Blob to read from. name is an 182 | // optional parameter which is present will override the actual name of 183 | // the blob provided. 184 | self.setBlob = (blob, name) => { 185 | self.__blob__ = blob; 186 | self.__name__ = name || blob.name || self.__name__ || 'Unknown'; 187 | self.element.querySelector('.info .headline').textContent = self.__name__; 188 | self.element.querySelector('.info .overline').textContent = filesize(blob.size); 189 | self.setImage(blob).catch(() => {}); 190 | } 191 | 192 | self.setState = (state, message) => { 193 | if (state) self.element.setAttribute('state', state); 194 | if (message) self.element.querySelector('.info .text').textContent = message; 195 | } 196 | 197 | // remove will remove the animate and remove the element from the page. 198 | // TODO: also revoke thumbnail URLs 199 | self.remove = () => { 200 | self.element.removeAttribute('rendered'); 201 | const l = self.element.addEventListener('transitionend', e => { 202 | if (e.target !== self.element) return; 203 | self.element.removeEventListener('transitionend', l); 204 | self.element.remove(); 205 | fileElements.splice(fileElements.indexOf(self), 1); 206 | }); 207 | } 208 | 209 | self.upload = async () => { 210 | var iv, key, blob; 211 | if (self.encryption) { 212 | self.setState('encrypting', 'Encrypting file'); 213 | try { 214 | [iv, key, blob] = await encrypt(await (new Response(self.__blob__).arrayBuffer())); 215 | } catch(e) { return self.setState('error', e || 'Unknown error') } 216 | } 217 | 218 | self.setState('uploading', 'Upload starting') 219 | 220 | blob = blob || self.__blob__; 221 | 222 | const data = new FormData(); 223 | data.append('file', blob, self.__name__) 224 | 225 | const req = new XMLHttpRequest(); 226 | 227 | var cancelled = false; 228 | self.element.querySelector('.buttons button.primary').onclick = () => { 229 | cancelled = true; 230 | req.abort(); 231 | self.element.querySelector('.buttons button.primary').onclick = self.remove; 232 | } 233 | 234 | req.upload.onprogress = e => (e.lengthComputable && !cancelled) && self.setState('uploading', 'Uploading ' + ((e.loaded / e.total * 100)|0) + '%'); 235 | 236 | const err = () => { 237 | self.setState('error', cancelled ? 'Cancelled' : (req.statusText || req.status || 'Unknown error')); 238 | self.element.querySelector('.buttons button.primary').onclick = self.remove; 239 | } 240 | 241 | req.onreadystatechange = function(e) { 242 | if (this.readyState === 4) return err(); 243 | if (this.readyState !== 2) return; 244 | this.onreadystatechange = null; 245 | if (this.status !== 200) return err(); 246 | var u = this.responseURL; 247 | const a = document.createElement('a'); 248 | a.href = u; 249 | if (self.encryption) { 250 | a.hash = encode(iv.concat(key)) + a.pathname; 251 | a.pathname = 'private'; 252 | u = a.href; 253 | } 254 | self.expires = new Date(this.getResponseHeader('Expires')); 255 | self.element.querySelector('.buttons button.secondary').onclick = self.remove; 256 | self.element.querySelector('.buttons button.primary').onclick = e => { 257 | const el = importTemplate(document.getElementById('share-template')); 258 | const remove = () => { 259 | el.classList.remove('open'); 260 | el.addEventListener('transitionend', e => e.target === el && el.remove()); 261 | } 262 | const undarken = darken(remove); 263 | // Set remove listener 264 | el.addEventListener('click', e => (e.target.classList.contains('item') || e.target.parentElement.classList.contains('item')) && undarken()); 265 | // Set URL 266 | const elt = el.querySelector('.extra'); 267 | elt.value = u; 268 | // Open item 269 | el.querySelector('.item.open').onclick = () => window.open(u, '_blank'); 270 | // Copy item 271 | el.querySelector('.item.copy').onclick = () => { 272 | elt.focus(); 273 | elt.select(); 274 | document.execCommand('Copy'); 275 | } 276 | // QR item 277 | el.querySelector('.item.qr').onclick = () => { 278 | qr.set({value: u}) 279 | dialog( 280 | 'QR code', 281 | el => el.appendChild(qr.canvas), 282 | [{ text: 'Close' }] 283 | ); 284 | } 285 | // More item 286 | el.querySelector('.item.more').onclick = () => navigator.share({ 287 | title: self.__name__, 288 | url: u 289 | }); 290 | if (!navigator.share) 291 | el.querySelector('.item.more').remove(); 292 | // Append template 293 | document.body.getElementsByTagName('main')[0].appendChild(el); 294 | elt.focus(); 295 | elt.select(); 296 | // Render template 297 | requestAnimationFrame(() => el.classList.add('open')); 298 | } 299 | // we only want the headers 300 | this.abort(); 301 | } 302 | 303 | req.open('POST', '/', true); 304 | try { req.send(data); } catch(e) { err(e); } 305 | } 306 | 307 | // import template and start rendering. 308 | self.element = importTemplate(document.getElementById('file-template')); 309 | 310 | // Add secure class name if the file is encrypted. 311 | if (self.encryption) self.element.classList.add('secure'); 312 | 313 | // Append element to body and render. 314 | (f => f.insertBefore(self.element, f.firstElementChild.nextElementSibling))(document.querySelector('main')); 315 | 316 | // Push element into 'global' array for rendering. 317 | fileElements.push(self); 318 | 319 | // Set the initial Blob. 320 | self.setBlob(blob, name); 321 | 322 | return self; 323 | } 324 | 325 | function processFiles(files) { 326 | if (!files.length) return; 327 | undarken && undarken(); 328 | if (files.length === 1) return (new FileElement(files[0])).upload(); 329 | dialog('Archive these files?', 'An archive of files will be created and uploaded rather than uploading each file individually.', [ 330 | { text: 'No thanks', f: () => files.forEach(f => (new FileElement(f)).upload())}, 331 | { text: 'Archive', f: async () => { 332 | const f = new FileElement(new Blob(), encode(crypto.getRandomValues(new Uint8Array(6))) + '.zip'); 333 | f.setState('archiving', 'Archive starting'); 334 | 335 | try { 336 | (is = i => f.setImage(files[i]).catch(e => (++i < files.length) && requestAnimationFrame(is.bind(null, i))))(0); 337 | 338 | const zip = new JSZip(); 339 | await files.reduce((acc, file, i) => acc.then(async acc => { 340 | const result = await (new Response(file).arrayBuffer()); 341 | zip.file(file.name, result); 342 | f.setState('archiving', 'Archiving ' + (i+1) + ' of ' + files.length + ' files'); 343 | f.element.querySelector('.info .overline').textContent = filesize(acc += result.byteLength); 344 | return acc; 345 | }), Promise.resolve(0)); 346 | f.setBlob(await zip.generateAsync({ type: 'blob' })); 347 | f.upload(); 348 | } catch(e) { 349 | f.setState('error', e || 'Unknown error'); 350 | f.element.querySelector('.buttons button.primary').onclick = f.remove; 351 | } 352 | } 353 | }]); 354 | } 355 | 356 | const processTransfer = async transfer => { 357 | if (transfer.files.length) return processFiles(Array.from(transfer.files)); 358 | if (!transfer.items.length) return; 359 | var item = transfer.items[0], name = encode(crypto.getRandomValues(new Uint8Array(6))); 360 | const f = (i, e) => (i.name = name + '.' + (e || i.type.split('/')[1]), processFiles([i])); 361 | const utf8bytes = s => Uint8Array.from(s.split('').map(c => c.charCodeAt(0))); 362 | if (item.kind === 'file') f(item.getAsFile()); 363 | else if (item.kind === 'string') f(new Blob([utf8bytes(await new Promise((resolve, reject) => item.getAsString(resolve)))]), 'txt'); 364 | } 365 | 366 | const preventDefault = e => (e.preventDefault(), false); 367 | 368 | document.addEventListener('dragover', preventDefault); 369 | document.addEventListener('dragenter', preventDefault); 370 | document.addEventListener('dragend', preventDefault); 371 | document.addEventListener('dragleave', preventDefault); 372 | document.addEventListener('drop', e => { 373 | e.preventDefault(); 374 | processTransfer(e.dataTransfer); 375 | return false; 376 | }); 377 | document.addEventListener('paste', e => processTransfer(e.clipboardData)); 378 | 379 | document.querySelector('#fab input').addEventListener('change', e => { 380 | processFiles(Array.from(e.target.files)); 381 | e.target.value = null; 382 | }); 383 | 384 | // https://github.com/odyniec/tinyAgo-js 385 | const ago = v => {v=0|(Date.now()-v)/1e3;var a,b={second:60,minute:60,hour:24,day:7,week:4.35,month:12,year:1e4},c;for(a in b){c=v%b[a];if(!(v=0|v/b[a]))return c+' '+(c-1?a+'s':a)}} 386 | 387 | (function render() { 388 | requestAnimationFrame(render); 389 | fileElements.forEach(f => { 390 | if (!f.rendered) f.element.setAttribute('rendered', f.rendered = true); 391 | if (!f.expires) return; 392 | if (!+f.expires) f.setState('done', 'Permanently uploaded'); 393 | else if (f.expires >= new Date()) f.setState('done', 'Expires in ' + ago(2 * new Date() - f.expires)); 394 | else { 395 | f.expires = null; 396 | f.setState('error', 'Expired'); 397 | f.element.querySelector('.info .headline').removeAttribute('href'); 398 | f.element.querySelector('.buttons button.primary').onclick = f.remove; 399 | } 400 | }); 401 | })(); 402 | })(); 403 | -------------------------------------------------------------------------------- /web/js/pica.min.js: -------------------------------------------------------------------------------- 1 | !function(t){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=t();else if("function"==typeof define&&define.amd)define([],t);else{("undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this).pica=t()}}(function(){return function t(e,i,r){function n(A,a){if(!i[A]){if(!e[A]){var s="function"==typeof require&&require;if(!a&&s)return s(A,!0);if(o)return o(A,!0);var u=new Error("Cannot find module '"+A+"'");throw u.code="MODULE_NOT_FOUND",u}var h=i[A]={exports:{}};e[A][0].call(h.exports,function(t){var i=e[A][1][t];return n(i||t)},h,h.exports,t,e,i,r)}return i[A].exports}for(var o="function"==typeof require&&require,A=0;A=0,wasm:e.indexOf("wasm")>=0};o.call(this,i),this.features={js:i.js,wasm:i.wasm&&this.has_wasm},this.use(A),this.use(a)}var n=t("inherits"),o=t("multimath"),A=t("multimath/lib/unsharp_mask"),a=t("./mm_resize");n(r,o),r.prototype.resizeAndUnsharp=function(t,e){var i=this.resize(t,e);return t.unsharpAmount&&this.unsharp_mask(i,t.toWidth,t.toHeight,t.unsharpAmount,t.unsharpRadius,t.unsharpThreshold),i},e.exports=r},{"./mm_resize":4,inherits:14,multimath:15,"multimath/lib/unsharp_mask":18}],2:[function(t,e,i){"use strict";function r(t){return t<0?0:t>255?255:t}e.exports={convolveHorizontally:function(t,e,i,n,o,A){var a,s,u,h,f,g,c,l,d,I,m,p=0,w=0;for(d=0;d0;c--)h=h+(m=A[f++])*t[l+3]|0,u=u+m*t[l+2]|0,s=s+m*t[l+1]|0,a=a+m*t[l]|0,l=l+4|0;e[w+3]=r(h+8192>>14),e[w+2]=r(u+8192>>14),e[w+1]=r(s+8192>>14),e[w]=r(a+8192>>14),w=w+4*n|0}w=4*(d+1)|0,p=(d+1)*i*4|0}},convolveVertically:function(t,e,i,n,o,A){var a,s,u,h,f,g,c,l,d,I,m,p=0,w=0;for(d=0;d0;c--)h=h+(m=A[f++])*t[l+3]|0,u=u+m*t[l+2]|0,s=s+m*t[l+1]|0,a=a+m*t[l]|0,l=l+4|0;e[w+3]=r(h+8192>>14),e[w+2]=r(u+8192>>14),e[w+1]=r(s+8192>>14),e[w]=r(a+8192>>14),w=w+4*n|0}w=4*(d+1)|0,p=(d+1)*i*4|0}}}},{}],3:[function(t,e,i){"use strict";e.exports="AGFzbQEAAAABFAJgBn9/f39/fwBgB39/f39/f38AAg8BA2VudgZtZW1vcnkCAAEDAwIAAQQEAXAAAAcZAghjb252b2x2ZQAACmNvbnZvbHZlSFYAAQkBAArmAwLBAwEQfwJAIANFDQAgBEUNACAFQQRqIRVBACEMQQAhDQNAIA0hDkEAIRFBACEHA0AgB0ECaiESAn8gBSAHQQF0IgdqIgZBAmouAQAiEwRAQQAhCEEAIBNrIRQgFSAHaiEPIAAgDCAGLgEAakECdGohEEEAIQlBACEKQQAhCwNAIBAoAgAiB0EYdiAPLgEAIgZsIAtqIQsgB0H/AXEgBmwgCGohCCAHQRB2Qf8BcSAGbCAKaiEKIAdBCHZB/wFxIAZsIAlqIQkgD0ECaiEPIBBBBGohECAUQQFqIhQNAAsgEiATagwBC0EAIQtBACEKQQAhCUEAIQggEgshByABIA5BAnRqIApBgMAAakEOdSIGQf8BIAZB/wFIG0EQdEGAgPwHcUEAIAZBAEobIAtBgMAAakEOdSIGQf8BIAZB/wFIG0EYdEEAIAZBAEobciAJQYDAAGpBDnUiBkH/ASAGQf8BSBtBCHRBgP4DcUEAIAZBAEobciAIQYDAAGpBDnUiBkH/ASAGQf8BSBtB/wFxQQAgBkEAShtyNgIAIA4gA2ohDiARQQFqIhEgBEcNAAsgDCACaiEMIA1BAWoiDSADRw0ACwsLIQACQEEAIAIgAyAEIAUgABAAIAJBACAEIAUgBiABEAALCw=="},{}],4:[function(t,e,i){"use strict";e.exports={name:"resize",fn:t("./resize"),wasm_fn:t("./resize_wasm"),wasm_src:t("./convolve_wasm_base64")}},{"./convolve_wasm_base64":3,"./resize":5,"./resize_wasm":8}],5:[function(t,e,i){"use strict";function r(t,e,i){for(var r=3,n=e*i*4|0;r>1]+=r(1-p),B=0;B0&&0===c[b];)b--;if(E=u+B,_=b-B+1,D[U++]=E,D[U++]=_,M)for(I=B;I<=b;I++)D[U++]=c[I];else D.set(c.subarray(B,b+1),U),U+=_}else D[U++]=0,D[U++]=0}return D}},{"./resize_filter_info":7}],7:[function(t,e,i){"use strict";e.exports=[{win:.5,filter:function(t){return t>=-.5&&t<.5?1:0}},{win:1,filter:function(t){if(t<=-1||t>=1)return 0;if(t>-1.1920929e-7&&t<1.1920929e-7)return 1;var e=t*Math.PI;return Math.sin(e)/e*(.54+.46*Math.cos(e/1))}},{win:2,filter:function(t){if(t<=-2||t>=2)return 0;if(t>-1.1920929e-7&&t<1.1920929e-7)return 1;var e=t*Math.PI;return Math.sin(e)/e*Math.sin(e/2)/(e/2)}},{win:3,filter:function(t){if(t<=-3||t>=3)return 0;if(t>-1.1920929e-7&&t<1.1920929e-7)return 1;var e=t*Math.PI;return Math.sin(e)/e*Math.sin(e/3)/(e/3)}}]},{}],8:[function(t,e,i){"use strict";function r(t,e,i){for(var r=3,n=e*i*4|0;r>8&255}}var A=t("./resize_filter_gen"),a=!0;try{a=1===new Uint32Array(new Uint8Array([1,0,0,0]).buffer)[0]}catch(t){}e.exports=function(t){var e=t.src,i=t.width,n=t.height,a=t.toWidth,s=t.toHeight,u=t.scaleX||t.toWidth/t.width,h=t.scaleY||t.toHeight/t.height,f=t.offsetX||0,g=t.offsetY||0,c=t.dest||new Uint8Array(a*s*4),l=void 0===t.quality?3:t.quality,d=t.alpha||!1,I=A(l,i,a,u,f),m=A(l,n,s,h,g),p=this.__align(0+Math.max(e.byteLength,c.byteLength),8),w=this.__align(p+n*a*4,8),B=this.__align(w+I.byteLength,8),b=B+m.byteLength,E=this.__instance("resize",b),_=new Uint8Array(this.__memory.buffer),y=new Uint32Array(this.__memory.buffer),Q=new Uint32Array(e.buffer);return y.set(Q),o(I,_,w),o(m,_,B),(E.exports.convolveHV||E.exports._convolveHV)(w,B,p,i,n,a,s),new Uint32Array(c.buffer).set(new Uint32Array(this.__memory.buffer,0,s*a)),d||r(c,a,s),c}},{"./resize_filter_gen":6}],9:[function(t,e,i){"use strict";function r(t,e){this.create=t,this.available=[],this.acquired={},this.lastId=1,this.timeoutId=0,this.idle=e||2e3}r.prototype.acquire=function(){var t=this,e=void 0;return 0!==this.available.length?e=this.available.pop():((e=this.create()).id=this.lastId++,e.release=function(){return t.release(e)}),this.acquired[e.id]=e,e},r.prototype.release=function(t){var e=this;delete this.acquired[t.id],t.lastUsed=Date.now(),this.available.push(t),0===this.timeoutId&&(this.timeoutId=setTimeout(function(){return e.gc()},100))},r.prototype.gc=function(){var t=this,e=Date.now();this.available=this.available.filter(function(i){return!(e-i.lastUsed>t.idle)||(i.destroy(),!1)}),0!==this.available.length?this.timeoutId=setTimeout(function(){return t.gc()},100):this.timeoutId=0},e.exports=r},{}],10:[function(t,e,i){"use strict";function r(t){var e=Math.round(t);return Math.abs(t-e)=t.toWidth&&(a=t.toWidth-e),(i=A-t.destTileBorder)<0&&(i=0),i+(s=A+c+t.destTileBorder-i)>=t.toHeight&&(s=t.toHeight-i),u={toX:e,toY:i,toWidth:a,toHeight:s,toInnerX:o,toInnerY:A,toInnerWidth:g,toInnerHeight:c,offsetX:e/h-r(e/h),offsetY:i/f-r(i/f),scaleX:h,scaleY:f,x:r(e/h),y:r(i/f),width:n(a/h),height:n(s/f)},l.push(u);return l}},{}],11:[function(t,e,i){"use strict";function r(t){return Object.prototype.toString.call(t)}e.exports.isCanvas=function(t){var e=r(t);return"[object HTMLCanvasElement]"===e||"[object Canvas]"===e},e.exports.isImage=function(t){return"[object HTMLImageElement]"===r(t)},e.exports.limiter=function(t){function e(){i=0;d--)s=a*I+A*m+u*p+h*w,h=u,u=s,A=a,a=t[f],e[g]=i[c]+u,f--,c--,g-=o}}var o,A,a,s,u,h,f,g;e.exports=function(t,e,i,o){if(o){var A=new Uint16Array(t.length),a=new Float32Array(Math.max(e,i)),s=r(o);n(t,A,a,s,e,i,o),n(A,t,a,s,i,e,o)}}},{}],14:[function(t,e,i){"function"==typeof Object.create?e.exports=function(t,e){t.super_=e,t.prototype=Object.create(e.prototype,{constructor:{value:t,enumerable:!1,writable:!0,configurable:!0}})}:e.exports=function(t,e){t.super_=e;var i=function(){};i.prototype=e.prototype,t.prototype=new i,t.prototype.constructor=t}},{}],15:[function(t,e,i){"use strict";function r(t){if(!(this instanceof r))return new r(t);var e=n({},a,t||{});if(this.options=e,this.__cache={},this.has_wasm=A(),this.__init_promise=null,this.__modules=e.modules||{},this.__memory=null,this.__wasm={},this.__isLE=1===new Uint32Array(new Uint8Array([1,0,0,0]).buffer)[0],!this.options.js&&!this.options.wasm)throw new Error('mathlib: at least "js" or "wasm" should be enabled')}var n=t("object-assign"),o=t("./lib/base64decode"),A=t("./lib/wa_detect"),a={js:!0,wasm:!0};r.prototype.use=function(t){return this.__modules[t.name]=t,this.has_wasm&&this.options.wasm&&t.wasm_fn?this[t.name]=t.wasm_fn:this[t.name]=t.fn,this},r.prototype.init=function(){if(this.__init_promise)return this.__init_promise;if(!this.options.js&&this.options.wasm&&!this.has_wasm)return Promise.reject(new Error('mathlib: only "wasm" was enabled, but it\'s not supported'));var t=this;return this.__init_promise=Promise.all(Object.keys(t.__modules).map(function(e){var i=t.__modules[e];return t.has_wasm&&t.options.wasm&&i.wasm_fn?t.__wasm[e]?null:WebAssembly.compile(t.__base64decode(i.wasm_src)).then(function(i){t.__wasm[e]=i}):null})).then(function(){return t}),this.__init_promise},r.prototype.__base64decode=o,r.prototype.__reallocate=function(t){if(!this.__memory)return this.__memory=new WebAssembly.Memory({initial:Math.ceil(t/65536)}),this.__memory;var e=this.__memory.buffer.byteLength;return e>2),n=0,o=0,A=0;A>16&255,r[o++]=n>>8&255,r[o++]=255&n),n=n<<6|"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".indexOf(e.charAt(A));var a=i%4*6;return 0===a?(r[o++]=n>>16&255,r[o++]=n>>8&255,r[o++]=255&n):18===a?(r[o++]=n>>10&255,r[o++]=n>>2&255):12===a&&(r[o++]=n>>4&255),r}},{}],17:[function(t,e,i){"use strict";e.exports=function(t,e,i){for(var r,n,o,A,a,s=e*i,u=new Uint16Array(s),h=0;h=n&&r>=o?r:n>=o&&n>=r?n:o,A=r<=n&&r<=o?r:n<=o&&n<=r?n:o,u[h]=257*(a+A)>>1;return u}},{}],18:[function(t,e,i){"use strict";e.exports={name:"unsharp_mask",fn:t("./unsharp_mask"),wasm_fn:t("./unsharp_mask_wasm"),wasm_src:t("./unsharp_mask_wasm_base64")}},{"./unsharp_mask":19,"./unsharp_mask_wasm":20,"./unsharp_mask_wasm_base64":21}],19:[function(t,e,i){"use strict";var r=t("glur/mono16"),n=t("./hsl_l16");e.exports=function(t,e,i,o,A,a){var s,u,h,f,g,c,l,d,I,m,p,w,B;if(!(0===o||A<.5)){A>2&&(A=2);var b=n(t,e,i),E=new Uint16Array(b);r(E,e,i,A);for(var _=o/100*4096+.5|0,y=257*a|0,Q=e*i,C=0;C=y&&(s=t[B=4*C],u=t[B+1],h=t[B+2],c=257*((d=s>=u&&s>=h?s:u>=s&&u>=h?u:h)+(l=s<=u&&s<=h?s:u<=s&&u<=h?u:h))>>1,l===d?f=g=0:(g=c<=32767?4095*(d-l)/(d+l)|0:4095*(d-l)/(510-d-l)|0,f=s===d?65535*(u-h)/(6*(d-l))|0:u===d?21845+(65535*(h-s)/(6*(d-l))|0):43690+(65535*(s-u)/(6*(d-l))|0)),(c+=_*w+2048>>12)>65535?c=65535:c<0&&(c=0),0===g?s=u=h=c>>8:(I=2*c-(m=c<=32767?c*(4096+g)+2048>>12:c+((65535-c)*g+2048>>12))>>8,m>>=8,s=(p=f+21845&65535)>=43690?I:p>=32767?I+(6*(m-I)*(43690-p)+32768>>16):p>=10922?m:I+(6*(m-I)*p+32768>>16),u=(p=65535&f)>=43690?I:p>=32767?I+(6*(m-I)*(43690-p)+32768>>16):p>=10922?m:I+(6*(m-I)*p+32768>>16),h=(p=f-21845&65535)>=43690?I:p>=32767?I+(6*(m-I)*(43690-p)+32768>>16):p>=10922?m:I+(6*(m-I)*p+32768>>16)),t[B]=s,t[B+1]=u,t[B+2]=h)}}},{"./hsl_l16":17,"glur/mono16":13}],20:[function(t,e,i){"use strict";e.exports=function(t,e,i,r,n,o){if(!(0===r||n<.5)){n>2&&(n=2);var A=e*i,a=4*A,s=2*A,u=2*A,h=4*Math.max(e,i),f=a,g=f+s,c=g+u,l=c+u,d=l+h,I=this.__instance("unsharp_mask",a+s+2*u+h+32,{exp:Math.exp}),m=new Uint32Array(t.buffer);new Uint32Array(this.__memory.buffer).set(m);var p=I.exports.hsl_l16||I.exports._hsl_l16;p(0,f,e,i),(p=I.exports.blurMono16||I.exports._blurMono16)(f,g,c,l,d,e,i,n),(p=I.exports.unsharp||I.exports._unsharp)(0,0,f,g,e,i,r,o),m.set(new Uint32Array(this.__memory.buffer,0,A))}}},{}],21:[function(t,e,i){"use strict";e.exports="AGFzbQEAAAABMQZgAXwBfGACfX8AYAZ/f39/f38AYAh/f39/f39/fQBgBH9/f38AYAh/f39/f39/fwACGQIDZW52A2V4cAAAA2VudgZtZW1vcnkCAAEDBgUBAgMEBQQEAXAAAAdMBRZfX2J1aWxkX2dhdXNzaWFuX2NvZWZzAAEOX19nYXVzczE2X2xpbmUAAgpibHVyTW9ubzE2AAMHaHNsX2wxNgAEB3Vuc2hhcnAABQkBAAqJEAXZAQEGfAJAIAFE24a6Q4Ia+z8gALujIgOaEAAiBCAEoCIGtjgCECABIANEAAAAAAAAAMCiEAAiBbaMOAIUIAFEAAAAAAAA8D8gBKEiAiACoiAEIAMgA6CiRAAAAAAAAPA/oCAFoaMiArY4AgAgASAEIANEAAAAAAAA8L+gIAKioiIHtjgCBCABIAQgA0QAAAAAAADwP6AgAqKiIgO2OAIIIAEgBSACoiIEtow4AgwgASACIAegIAVEAAAAAAAA8D8gBqGgIgKjtjgCGCABIAMgBKEgAqO2OAIcCwu3AwMDfwR9CHwCQCADKgIUIQkgAyoCECEKIAMqAgwhCyADKgIIIQwCQCAEQX9qIgdBAEgiCA0AIAIgAC8BALgiDSADKgIYu6IiDiAJuyIQoiAOIAq7IhGiIA0gAyoCBLsiEqIgAyoCALsiEyANoqCgoCIPtjgCACACQQRqIQIgAEECaiEAIAdFDQAgBCEGA0AgAiAOIBCiIA8iDiARoiANIBKiIBMgAC8BALgiDaKgoKAiD7Y4AgAgAkEEaiECIABBAmohACAGQX9qIgZBAUoNAAsLAkAgCA0AIAEgByAFbEEBdGogAEF+ai8BACIIuCINIAu7IhGiIA0gDLsiEqKgIA0gAyoCHLuiIg4gCrsiE6KgIA4gCbsiFKKgIg8gAkF8aioCALugqzsBACAHRQ0AIAJBeGohAiAAQXxqIQBBACAFQQF0ayEHIAEgBSAEQQF0QXxqbGohBgNAIAghAyAALwEAIQggBiANIBGiIAO4Ig0gEqKgIA8iECAToqAgDiAUoqAiDyACKgIAu6CrOwEAIAYgB2ohBiAAQX5qIQAgAkF8aiECIBAhDiAEQX9qIgRBAUoNAAsLCwvfAgIDfwZ8AkAgB0MAAAAAWw0AIARE24a6Q4Ia+z8gB0MAAAA/l7ujIgyaEAAiDSANoCIPtjgCECAEIAxEAAAAAAAAAMCiEAAiDraMOAIUIAREAAAAAAAA8D8gDaEiCyALoiANIAwgDKCiRAAAAAAAAPA/oCAOoaMiC7Y4AgAgBCANIAxEAAAAAAAA8L+gIAuioiIQtjgCBCAEIA0gDEQAAAAAAADwP6AgC6KiIgy2OAIIIAQgDiALoiINtow4AgwgBCALIBCgIA5EAAAAAAAA8D8gD6GgIgujtjgCGCAEIAwgDaEgC6O2OAIcIAYEQCAFQQF0IQogBiEJIAIhCANAIAAgCCADIAQgBSAGEAIgACAKaiEAIAhBAmohCCAJQX9qIgkNAAsLIAVFDQAgBkEBdCEIIAUhAANAIAIgASADIAQgBiAFEAIgAiAIaiECIAFBAmohASAAQX9qIgANAAsLC7wBAQV/IAMgAmwiAwRAQQAgA2shBgNAIAAoAgAiBEEIdiIHQf8BcSECAn8gBEH/AXEiAyAEQRB2IgRB/wFxIgVPBEAgAyIIIAMgAk8NARoLIAQgBCAHIAIgA0kbIAIgBUkbQf8BcQshCAJAIAMgAk0EQCADIAVNDQELIAQgByAEIAMgAk8bIAIgBUsbQf8BcSEDCyAAQQRqIQAgASADIAhqQYECbEEBdjsBACABQQJqIQEgBkEBaiIGDQALCwvTBgEKfwJAIAazQwAAgEWUQwAAyEKVu0QAAAAAAADgP6CqIQ0gBSAEbCILBEAgB0GBAmwhDgNAQQAgAi8BACADLwEAayIGQQF0IgdrIAcgBkEASBsgDk8EQCAAQQJqLQAAIQUCfyAALQAAIgYgAEEBai0AACIESSIJRQRAIAYiCCAGIAVPDQEaCyAFIAUgBCAEIAVJGyAGIARLGwshCAJ/IAYgBE0EQCAGIgogBiAFTQ0BGgsgBSAFIAQgBCAFSxsgCRsLIgogCGoiD0GBAmwiEEEBdiERQQAhDAJ/QQAiCSAIIApGDQAaIAggCmsiCUH/H2wgD0H+AyAIayAKayAQQYCABEkbbSEMIAYgCEYEQCAEIAVrQf//A2wgCUEGbG0MAQsgBSAGayAGIARrIAQgCEYiBhtB//8DbCAJQQZsbUHVqgFBqtUCIAYbagshCSARIAcgDWxBgBBqQQx1aiIGQQAgBkEAShsiBkH//wMgBkH//wNIGyEGAkACfwJAIAxB//8DcSIFBEAgBkH//wFKDQEgBUGAIGogBmxBgBBqQQx2DAILIAZBCHYiBiEFIAYhBAwCCyAFIAZB//8Dc2xBgBBqQQx2IAZqCyIFQQh2IQcgBkEBdCAFa0EIdiIGIQQCQCAJQdWqAWpB//8DcSIFQanVAksNACAFQf//AU8EQEGq1QIgBWsgByAGa2xBBmxBgIACakEQdiAGaiEEDAELIAchBCAFQanVAEsNACAFIAcgBmtsQQZsQYCAAmpBEHYgBmohBAsCfyAGIgUgCUH//wNxIghBqdUCSw0AGkGq1QIgCGsgByAGa2xBBmxBgIACakEQdiAGaiAIQf//AU8NABogByIFIAhBqdUASw0AGiAIIAcgBmtsQQZsQYCAAmpBEHYgBmoLIQUgCUGr1QJqQf//A3EiCEGp1QJLDQAgCEH//wFPBEBBqtUCIAhrIAcgBmtsQQZsQYCAAmpBEHYgBmohBgwBCyAIQanVAEsEQCAHIQYMAQsgCCAHIAZrbEEGbEGAgAJqQRB2IAZqIQYLIAEgBDoAACABQQFqIAU6AAAgAUECaiAGOgAACyADQQJqIQMgAkECaiECIABBBGohACABQQRqIQEgC0F/aiILDQALCwsL"},{}],22:[function(t,e,i){"use strict";var r,n=t("./base64decode");e.exports=function(){if(void 0!==r)return r;if(r=!1,"undefined"==typeof WebAssembly)return r;try{var t=new WebAssembly.Module(n("AGFzbQEAAAABBQFgAAF/Ag8BA2VudgZtZW1vcnkCAAEDAgEABAQBcAAABwoBBmRldGVjdAAACQEACgYBBABBAQs=")),e={memoryBase:0,memory:new WebAssembly.Memory({initial:1}),tableBase:0,table:new WebAssembly.Table({initial:0,element:"anyfunc"})};return 1===(0,new WebAssembly.Instance(t,{env:e}).exports.detect)()&&(r=!0),r}catch(t){}return r}},{"./base64decode":16}],23:[function(t,e,i){"use strict";function r(t){if(null===t||void 0===t)throw new TypeError("Object.assign cannot be called with null or undefined");return Object(t)}var n=Object.getOwnPropertySymbols,o=Object.prototype.hasOwnProperty,A=Object.prototype.propertyIsEnumerable;e.exports=function(){try{if(!Object.assign)return!1;var t=new String("abc");if(t[5]="de","5"===Object.getOwnPropertyNames(t)[0])return!1;for(var e={},i=0;i<10;i++)e["_"+String.fromCharCode(i)]=i;if("0123456789"!==Object.getOwnPropertyNames(e).map(function(t){return e[t]}).join(""))return!1;var r={};return"abcdefghijklmnopqrst".split("").forEach(function(t){r[t]=t}),"abcdefghijklmnopqrst"===Object.keys(Object.assign({},r)).join("")}catch(t){return!1}}()?Object.assign:function(t,e){for(var i,a,s=r(t),u=1;u=0)}catch(t){}var l=1;"undefined"!=typeof navigator&&(l=Math.min(navigator.hardwareConcurrency||1,4));var d={tile:1024,concurrency:l,features:["js","wasm","ww"],idle:2e3},I={quality:3,alpha:!1,unsharpAmount:0,unsharpRadius:0,unsharpThreshold:0},m=void 0,p=void 0;n.prototype.init=function(){var e=this;if(this.__initPromise)return this.__initPromise;if(!1!==m&&!0!==m&&(m=!1,"undefined"!=typeof ImageData&&"undefined"!=typeof Uint8ClampedArray))try{new ImageData(new Uint8ClampedArray(400),10,10),m=!0}catch(t){}!1!==p&&!0!==p&&(p=!1,"undefined"!=typeof ImageBitmap&&(ImageBitmap.prototype&&ImageBitmap.prototype.close?p=!0:this.debug("ImageBitmap does not support .close(), disabled")));var i=this.options.features.slice();if(i.indexOf("all")>=0&&(i=["cib","wasm","js","ww"]),this.__requested_features=i,this.__mathlib=new a(i),i.indexOf("ww")>=0&&"undefined"!=typeof window&&"Worker"in window)try{t("webworkify")(function(){}).terminate(),this.features.ww=!0;var n="wp_"+JSON.stringify(this.options);g[n]?this.__workersPool=g[n]:(this.__workersPool=new s(r,this.options.idle),g[n]=this.__workersPool)}catch(t){}var A=this.__mathlib.init().then(function(t){o(e.features,t.features)}),h=void 0;return h=p?u.cib_support().then(function(t){e.features.cib&&i.indexOf("cib")<0?e.debug("createImageBitmap() resize supported, but disabled by config"):i.indexOf("cib")>=0&&(e.features.cib=t)}):Promise.resolve(!1),this.__initPromise=Promise.all([A,h]).then(function(){return e}),this.__initPromise},n.prototype.resize=function(t,e,i){var r=this;this.debug("Start resize...");var n=I;isNaN(i)?i&&(n=o(n,i)):n=o(n,{quality:i}),n.toWidth=e.width,n.toHeigth=e.height,n.width=t.naturalWidth||t.width,n.height=t.naturalHeight||t.height;var A=!1,a=null;n.cancelToken&&(a=n.cancelToken.then(function(t){throw A=!0,t},function(t){throw A=!0,t}));var s=e.getContext("2d",{alpha:Boolean(n.alpha)});return this.init().then(function(){if(A)return a;if(r.features.cib)return r.debug("Resize via createImageBitmap()"),createImageBitmap(t,{resizeWidth:n.toWidth,resizeHeight:n.toHeigth,resizeQuality:u.cib_quality_name(n.quality)}).then(function(t){if(A)return a;if(!n.unsharpAmount)return s.drawImage(t,0,0),t.close(),s=null,r.debug("Finished!"),e;r.debug("Unsharp result");var i=document.createElement("canvas");i.width=n.toWidth,i.height=n.toHeigth;var o=i.getContext("2d",{alpha:Boolean(n.alpha)});o.drawImage(t,0,0),t.close();var u=o.getImageData(0,0,n.toWidth,n.toHeigth);return r.__mathlib.unsharp(u.data,n.toWidth,n.toHeigth,n.unsharpAmount,n.unsharpRadius,n.unsharpThreshold),s.putImageData(u,0,0),u=o=i=s=null,r.debug("Finished!"),e});var i=void 0,o=void 0,h={},g=function(t){return Promise.resolve().then(function(){return r.features.ww?new Promise(function(e,i){var n=r.__workersPool.acquire();a&&a.catch(function(t){return i(t)}),n.value.onmessage=function(t){n.release(),t.data.err?i(t.data.err):e(t.data.result)},n.value.postMessage({opts:t,features:r.__requested_features,preload:{wasm_nodule:r.__mathlib.__}},[t.src.buffer])}):r.__mathlib.resizeAndUnsharp(t,h)})},l=function(e){return r.__limit(function(){if(A)return a;var h=void 0;if(u.isCanvas(t))r.debug("Get tile pixel data"),h=i.getImageData(e.x,e.y,e.width,e.height);else{r.debug("Draw tile imageBitmap/image to temporary canvas");var f=document.createElement("canvas");f.width=e.width,f.height=e.height;var l=f.getContext("2d",{alpha:Boolean(n.alpha)});l.globalCompositeOperation="copy",l.drawImage(o||t,e.x,e.y,e.width,e.height,0,0,e.width,e.height),r.debug("Get tile pixel data"),h=l.getImageData(0,0,e.width,e.height),l=f=null}var d={src:h.data,width:e.width,height:e.height,toWidth:e.toWidth,toHeight:e.toHeight,scaleX:e.scaleX,scaleY:e.scaleY,offsetX:e.offsetX,offsetY:e.offsetY,quality:n.quality,alpha:n.alpha,unsharpAmount:n.unsharpAmount,unsharpRadius:n.unsharpRadius,unsharpThreshold:n.unsharpThreshold};return r.debug("Invoke resize math"),Promise.resolve().then(function(){return g(d)}).then(function(t){if(A)return a;h=null;var i=void 0;if(r.debug("Convert raw rgba tile result to ImageData"),m)i=new ImageData(new Uint8ClampedArray(t),e.toWidth,e.toHeight);else if((i=s.createImageData(e.toWidth,e.toHeight)).data.set)i.data.set(t);else for(var n=i.data.length-1;n>=0;n--)i.data[n]=t[n];return r.debug("Draw tile"),c?s.putImageData(i,e.toX,e.toY,e.toInnerX-e.toX,e.toInnerY-e.toY,e.toInnerWidth+1e-5,e.toInnerHeight+1e-5):s.putImageData(i,e.toX,e.toY,e.toInnerX-e.toX,e.toInnerY-e.toY,e.toInnerWidth,e.toInnerHeight),null})})};return Promise.resolve().then(function(){if(u.isCanvas(t))return i=t.getContext("2d",{alpha:Boolean(n.alpha)}),null;if(u.isImage(t))return p?(r.debug("Decode image via createImageBitmap"),createImageBitmap(t).then(function(t){o=t})):null;throw new Error('".from" should be image or canvas')}).then(function(){function t(){o&&(o.close(),o=null)}if(A)return a;r.debug("Calculate tiles");var i=f({width:n.width,height:n.height,srcTileSize:r.options.tile,toWidth:n.toWidth,toHeight:n.toHeigth,destTileBorder:Math.ceil(Math.max(3,2.5*n.unsharpRadius|0))}).map(function(t){return l(t)});return r.debug("Process tiles"),Promise.all(i).then(function(){return r.debug("Finished!"),t(),e},function(e){throw t(),e})})})},n.prototype.resizeBuffer=function(t){var e=this,i=o(I,t);return this.init().then(function(){return e.__mathlib.resizeAndUnsharp(i)})},n.prototype.toBlob=function(t,e,i){return e=e||"image/png",new Promise(function(r){if(t.toBlob)t.toBlob(function(t){return r(t)},e,i);else{for(var n=atob(t.toDataURL(e,i).split(",")[1]),o=n.length,A=new Uint8Array(o),a=0;a>1&1,n=0;n0;e--)n[e]=n[e]?n[e-1]^_.EXPONENT[v._modN(_.LOG[n[e]]+t)]:n[e-1];n[0]=_.EXPONENT[v._modN(_.LOG[n[0]]+t)]}for(t=0;t<=i;t++)n[t]=_.LOG[n[t]]},_checkBadness:function(){var t,e,i,n,s,r=0,o=this._badness,a=this.buffer,h=this.width;for(s=0;sh*h;)u-=h*h,c++;for(r+=c*v.N4,n=0;n=o-2&&(t=o-2,s>9&&t--);var a=t;if(s>9){for(r[a+2]=0,r[a+3]=0;a--;)e=r[a],r[a+3]|=255&e<<4,r[a+2]=e>>4;r[2]|=255&t<<4,r[1]=t>>4,r[0]=64|t>>12}else{for(r[a+1]=0,r[a+2]=0;a--;)e=r[a],r[a+2]|=255&e<<4,r[a+1]=e>>4;r[1]|=255&t<<4,r[0]=64|t>>4}for(a=t+3-(s<10);a=5&&(i+=v.N1+n[e]-5);for(e=3;et||3*n[e-3]>=4*n[e]||3*n[e+3]>=4*n[e])&&(i+=v.N3);return i},_finish:function(){this._stringBuffer=this.buffer.slice();var t,e,i=0,n=3e4;for(e=0;e<8&&(this._applyMask(e),(t=this._checkBadness())>=1)1&n&&(s[r-1-e+8*r]=1,e<6?s[8+r*e]=1:s[8+r*(e+1)]=1);for(e=0;e<7;e++,n>>=1)1&n&&(s[8+r*(r-7+e)]=1,e?s[6-e+8*r]=1:s[7+8*r]=1)},_interleaveBlocks:function(){var t,e,i=this._dataBlock,n=this._ecc,s=this._eccBlock,r=0,o=this._calculateMaxLength(),a=this._neccBlock1,h=this._neccBlock2,f=this._stringBuffer;for(t=0;t1)for(t=u.BLOCK[n],i=s-7;;){for(e=s-7;e>t-3&&(this._addAlignment(e,i),!(e6)for(t=d.BLOCK[r-7],e=17,i=0;i<6;i++)for(n=0;n<3;n++,e--)1&(e>11?r>>e-12:t>>e)?(s[5-i+o*(2-n+o-11)]=1,s[2-n+o-11+o*(5-i)]=1):(this._setMask(5-i,2-n+o-11),this._setMask(2-n+o-11,5-i))},_isMasked:function(t,e){var i=v._getMaskBit(t,e);return 1===this._mask[i]},_pack:function(){var t,e,i,n=1,s=1,r=this.width,o=r-1,a=r-1,h=(this._dataBlock+this._eccBlock)*(this._neccBlock1+this._neccBlock2)+this._neccBlock2;for(e=0;ee&&(i=t,t=e,e=i),i=e,i+=e*e,i>>=1,i+=t},_modN:function(t){for(;t>=255;)t=((t-=255)>>8)+(255&t);return t},N1:3,N2:3,N3:40,N4:10}),p=v,m=f.extend({draw:function(){this.element.src=this.qrious.toDataURL()},reset:function(){this.element.src=""},resize:function(){var t=this.element;t.width=t.height=this.qrious.size}}),g=h.extend(function(t,e,i,n){this.name=t,this.modifiable=Boolean(e),this.defaultValue=i,this._valueTransformer=n},{transform:function(t){var e=this._valueTransformer;return"function"==typeof e?e(t,this):t}}),k=h.extend(null,{abs:function(t){return null!=t?Math.abs(t):null},hasOwn:function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},noop:function(){},toUpperCase:function(t){return null!=t?t.toUpperCase():null}}),w=h.extend(function(t){this.options={},t.forEach(function(t){this.options[t.name]=t},this)},{exists:function(t){return null!=this.options[t]},get:function(t,e){return w._get(this.options[t],e)},getAll:function(t){var e,i=this.options,n={};for(e in i)k.hasOwn(i,e)&&(n[e]=w._get(i[e],t));return n},init:function(t,e,i){"function"!=typeof i&&(i=k.noop);var n,s;for(n in this.options)k.hasOwn(this.options,n)&&(s=this.options[n],w._set(s,s.defaultValue,e),w._createAccessor(s,e,i));this._setAll(t,e,!0)},set:function(t,e,i){return this._set(t,e,i)},setAll:function(t,e){return this._setAll(t,e)},_set:function(t,e,i,n){var s=this.options[t];if(!s)throw new Error("Invalid option: "+t);if(!s.modifiable&&!n)throw new Error("Option cannot be modified: "+t);return w._set(s,e,i)},_setAll:function(t,e,i){if(!t)return!1;var n,s=!1;for(n in t)k.hasOwn(t,n)&&this._set(n,t[n],e,i)&&(s=!0);return s}},{_createAccessor:function(t,e,i){var n={get:function(){return w._get(t,e)}};t.modifiable&&(n.set=function(n){w._set(t,n,e)&&i(n,t)}),Object.defineProperty(e,t.name,n)},_get:function(t,e){return e["_"+t.name]},_set:function(t,e,i){var n="_"+t.name,s=i[n],r=t.transform(null!=e?e:t.defaultValue);return i[n]=r,r!==s}}),M=w,b=h.extend(function(){this._services={}},{getService:function(t){var e=this._services[t];if(!e)throw new Error("Service is not being managed with name: "+t);return e},setService:function(t,e){if(this._services[t])throw new Error("Service is already managed with name: "+t);e&&(this._services[t]=e)}}),B=new M([new g("background",!0,"white"),new g("backgroundAlpha",!0,1,k.abs),new g("element"),new g("foreground",!0,"black"),new g("foregroundAlpha",!0,1,k.abs),new g("level",!0,"L",k.toUpperCase),new g("mime",!0,"image/png"),new g("padding",!0,null,k.abs),new g("size",!0,100,k.abs),new g("value",!0,"")]),y=new b,O=h.extend(function(t){B.init(t,this,this.update.bind(this));var e=B.get("element",this),i=y.getService("element"),n=e&&i.isCanvas(e)?e:i.createCanvas(),s=e&&i.isImage(e)?e:i.createImage();this._canvasRenderer=new c(this,n,!0),this._imageRenderer=new m(this,s,s===e),this.update()},{get:function(){return B.getAll(this)},set:function(t){B.setAll(t,this)&&this.update()},toDataURL:function(t){return this.canvas.toDataURL(t||this.mime)},update:function(){var t=new p({level:this.level,value:this.value});this._canvasRenderer.render(t),this._imageRenderer.render(t)}},{use:function(t){y.setService(t.getName(),t)}});Object.defineProperties(O.prototype,{canvas:{get:function(){return this._canvasRenderer.getElement()}},image:{get:function(){return this._imageRenderer.getElement()}}});var A=O,L=h.extend({getName:function(){}}).extend({createCanvas:function(){},createImage:function(){},getName:function(){return"element"},isCanvas:function(t){},isImage:function(t){}}).extend({createCanvas:function(){return document.createElement("canvas")},createImage:function(){return document.createElement("img")},isCanvas:function(t){return t instanceof HTMLCanvasElement},isImage:function(t){return t instanceof HTMLImageElement}});return A.use(new L),A}); 5 | 6 | //# sourceMappingURL=qrious.min.js.map -------------------------------------------------------------------------------- /web/js/stackblur.min.js: -------------------------------------------------------------------------------- 1 | !function(a){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=a();else if("function"==typeof define&&define.amd)define([],a);else{var b;b="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this,b.StackBlur=a()}}(function(){return function a(b,c,d){function e(g,h){if(!c[g]){if(!b[g]){var i="function"==typeof require&&require;if(!h&&i)return i(g,!0);if(f)return f(g,!0);var j=new Error("Cannot find module '"+g+"'");throw j.code="MODULE_NOT_FOUND",j}var k=c[g]={exports:{}};b[g][0].call(k.exports,function(a){var c=b[g][1][a];return e(c?c:a)},k,k.exports,a,b,c,d)}return c[g].exports}for(var f="function"==typeof require&&require,g=0;g>T,0!=F?(F=255/F,H[o]=(q*S>>T)*F,H[o+1]=(r*S>>T)*F,H[o+2]=(s*S>>T)*F):H[o]=H[o+1]=H[o+2]=0,q-=u,r-=v,s-=w,t-=x,u-=Q.r,v-=Q.g,w-=Q.b,x-=Q.a,m=p+((m=g+f+1)>T,F>0?(F=255/F,H[m]=(q*S>>T)*F,H[m+1]=(r*S>>T)*F,H[m+2]=(s*S>>T)*F):H[m]=H[m+1]=H[m+2]=0,q-=u,r-=v,s-=w,t-=x,u-=Q.r,v-=Q.g,w-=Q.b,x-=Q.a,m=g+((m=h+L)>P,D[o+1]=r*O>>P,D[o+2]=s*O>>P,q-=t,r-=u,s-=v,t-=M.r,u-=M.g,v-=M.b,m=p+((m=g+f+1)>P,D[m+1]=r*O>>P,D[m+2]=s*O>>P,q-=t,r-=u,s-=v,t-=M.r,u-=M.g,v-=M.b,m=g+((m=h+H) 2 | 3 | 4 | 5 | 6 | 7 | kipp 8 | 9 | 10 | 11 | 12 | 13 | 19 | 20 | 21 | 22 | 23 | 24 | 25 |
Not working? Try disabling your ad blocker
26 |
27 | Downloading 28 | Download 29 |
30 |
31 |
32 |
33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /web/private/js/main.js: -------------------------------------------------------------------------------- 1 | (async () => { 2 | const decrypt = async (iv, key, data) => crypto.subtle.decrypt({ name: 'AES-GCM', iv: iv }, await crypto.subtle.importKey('raw', key, { name: 'AES-GCM' }, false, ['decrypt']), data); 3 | 4 | const decode = s => Uint8Array.from(atob(s.replace(/-/g, '+').replace(/_/g, '/') + '=='), c => c.charCodeAt(0)); 5 | 6 | const s = location.hash.slice(1).split('/'); 7 | const b = decode(s[0]); 8 | const iv = new Uint8Array(12); 9 | const key = new Uint8Array(16); 10 | iv.set(b.slice(0, 12)); 11 | key.set(b.slice(12, 28)); 12 | 13 | const req = await new Promise(function(resolve, reject) { 14 | var req = new XMLHttpRequest(); 15 | req.onerror = reject; 16 | req.onload = () => { 17 | req.onprogress = null; 18 | if (req.status === 200) resolve(req); 19 | else reject(new Error('File has expired or is invalid')); 20 | } 21 | req.onprogress = e => document.querySelector('.progress .bar').style.width = ~~(e.loaded / e.total * 100) + '%'; 22 | req.open('GET', '/' + s[1], true); 23 | req.responseType = 'arraybuffer'; 24 | req.send(); 25 | }); 26 | 27 | document.querySelector('.progress .bar').style.width = '100%'; 28 | document.querySelector('.status .text.open').innerHTML = 'Decrypting'; 29 | 30 | const blob = new Blob([await decrypt(iv, key, req.response)], { type: req.getResponseHeader('Content-Type') }) 31 | const u = URL.createObjectURL(blob); 32 | const name = decodeURIComponent(req.getResponseHeader('Content-Disposition').split('"')[1].split('"')[0]); 33 | document.body.className = 'done'; 34 | document.querySelector('.status .text.open').innerHTML = 'Open'; 35 | 36 | var d = document.createElement('div'); 37 | d.className = 'info'; 38 | d.innerHTML = '
' + name + '
 ' + filesize(blob.size) + ''; 39 | document.querySelector('.status').appendChild(d); 40 | 41 | var a = document.querySelector('.status .text.open'); 42 | a.href = u; 43 | a.onclick = () => (window.open(u, '_blank').onunload = () => document.body.classList.add('suggest'), false); 44 | 45 | var ad = document.querySelector('.status .text.download'); 46 | ad.href = u; 47 | ad.download = name; 48 | })().catch(err => { 49 | document.body.className = 'error'; 50 | document.querySelector('.status .text.open').innerHTML = 'message' in err ? err.message : err; 51 | }); -------------------------------------------------------------------------------- /web/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: /private -------------------------------------------------------------------------------- /web/sharex: -------------------------------------------------------------------------------- 1 | { 2 | "Name": "kipp", 3 | "RequestType": "POST", 4 | "RequestURL": "https://kipp.6f.io", 5 | "FileFormName": "file", 6 | "ResponseType": "RedirectionURL" 7 | } --------------------------------------------------------------------------------