├── B1 ├── .gitignore ├── static │ ├── js │ │ └── qrcodejs │ │ │ ├── .gitignore │ │ │ ├── bower.json │ │ │ ├── LICENSE │ │ │ ├── index.svg │ │ │ ├── README.md │ │ │ ├── index.html │ │ │ └── index-svg.html │ ├── img │ │ ├── empty_QR.png │ │ ├── hotmaze_128.png │ │ ├── red_spinner.gif │ │ ├── schema_hot.png │ │ ├── transfer_to_cloud.png │ │ └── empty_QR_transfering.png │ ├── index.html │ ├── css │ │ └── hot-maze.css │ └── terms.html ├── app.yaml ├── go.mod ├── config │ └── bucket_cors.json ├── secrets.go ├── .gcloudignore ├── cors.go ├── server.go ├── cmd │ └── backend │ │ └── main.go ├── fileexpiry.go └── signedurls.go ├── B3 ├── .gitignore ├── static │ ├── js │ │ └── qrcodejs │ │ │ ├── .gitignore │ │ │ ├── bower.json │ │ │ ├── LICENSE │ │ │ ├── index.svg │ │ │ ├── README.md │ │ │ ├── index.html │ │ │ └── index-svg.html │ ├── img │ │ ├── empty_QR.png │ │ ├── hotmaze_128.png │ │ ├── red_spinner.gif │ │ ├── schema_hot.png │ │ ├── transfer_to_cloud.png │ │ └── empty_QR_transfering.png │ ├── index.html │ ├── css │ │ └── hot-maze.css │ └── terms.html ├── Dockerfile ├── config │ └── bucket_cors.json ├── go.mod ├── cors.go ├── secrets.go ├── .gcloudignore ├── server.go ├── fileexpiry.go ├── cmd │ └── backend │ │ └── main.go ├── signedurls.go └── term.go ├── C1 ├── .gitignore ├── static │ ├── js │ │ └── qrcodejs │ │ │ ├── .gitignore │ │ │ ├── bower.json │ │ │ ├── LICENSE │ │ │ ├── index.svg │ │ │ ├── README.md │ │ │ ├── index.html │ │ │ └── index-svg.html │ ├── img │ │ ├── empty_QR.png │ │ ├── hotmaze_128.png │ │ ├── red_spinner.gif │ │ ├── schema_hot.png │ │ ├── transfer_to_cloud.png │ │ └── empty_QR_transfering.png │ ├── index.html │ ├── css │ │ └── hot-maze.css │ └── terms.html ├── app.yaml ├── go.mod ├── cors.go ├── .gcloudignore ├── download.go ├── server.go ├── cmd │ └── backend │ │ └── main.go ├── upload.go └── fileexpiry.go ├── D1 ├── .gitignore ├── static │ ├── js │ │ └── qrcodejs │ │ │ ├── .gitignore │ │ │ ├── bower.json │ │ │ ├── LICENSE │ │ │ ├── index.svg │ │ │ ├── README.md │ │ │ ├── index.html │ │ │ └── index-svg.html │ ├── img │ │ ├── empty_QR.png │ │ ├── hotmaze_128.png │ │ ├── red_spinner.gif │ │ ├── schema_hot.png │ │ ├── transfer_to_cloud.png │ │ └── empty_QR_transfering.png │ ├── index.html │ ├── css │ │ └── hot-maze.css │ └── terms.html ├── app.yaml ├── go.mod ├── cors.go ├── .gcloudignore ├── server.go ├── cmd │ └── backend │ │ └── main.go ├── download.go ├── upload.go └── fileexpiry.go ├── A00 └── web │ ├── static │ ├── js │ │ ├── qrcodejs │ │ │ ├── .gitignore │ │ │ ├── bower.json │ │ │ ├── LICENSE │ │ │ ├── index.svg │ │ │ ├── README.md │ │ │ ├── index.html │ │ │ └── index-svg.html │ │ └── hot-maze.js │ ├── img │ │ ├── empty_QR.png │ │ ├── hotmaze_128.png │ │ ├── red_spinner.gif │ │ ├── schema_hot.png │ │ ├── transfer_to_cloud.png │ │ └── empty_QR_transfering.png │ └── css │ │ └── hot-maze.css │ └── index.html ├── B2 ├── frontend │ ├── public │ │ ├── js │ │ │ └── qrcodejs │ │ │ │ ├── .gitignore │ │ │ │ ├── bower.json │ │ │ │ ├── LICENSE │ │ │ │ ├── index.svg │ │ │ │ ├── README.md │ │ │ │ ├── index.html │ │ │ │ └── index-svg.html │ │ ├── img │ │ │ ├── empty_QR.png │ │ │ ├── schema_hot.png │ │ │ ├── hotmaze_128.png │ │ │ ├── red_spinner.gif │ │ │ ├── transfer_to_cloud.png │ │ │ └── empty_QR_transfering.png │ │ ├── 404.html │ │ ├── index.html │ │ ├── css │ │ │ └── hot-maze.css │ │ └── terms.html │ ├── .firebaserc │ ├── firebase.json │ └── .gitignore ├── backend │ ├── go.mod │ ├── server.go │ ├── config │ │ └── bucket_cors.json │ ├── secrets.go │ ├── cors.go │ ├── cmd │ │ └── backend │ │ │ └── main.go │ ├── functions.go │ ├── fileexpiry.go │ └── signedurls.go └── .gcloudignore ├── template └── web │ ├── static │ ├── js │ │ ├── qrcodejs │ │ │ ├── .gitignore │ │ │ ├── bower.json │ │ │ ├── LICENSE │ │ │ ├── index.svg │ │ │ ├── README.md │ │ │ ├── index.html │ │ │ └── index-svg.html │ │ └── hot-maze.js │ ├── img │ │ ├── empty_QR.png │ │ ├── hotmaze_128.png │ │ ├── red_spinner.gif │ │ ├── schema_hot.png │ │ ├── transfer_to_cloud.png │ │ └── empty_QR_transfering.png │ └── css │ │ └── hot-maze.css │ └── index.html └── README.md /B1/.gitignore: -------------------------------------------------------------------------------- 1 | /backend 2 | -------------------------------------------------------------------------------- /B3/.gitignore: -------------------------------------------------------------------------------- 1 | /backend 2 | -------------------------------------------------------------------------------- /C1/.gitignore: -------------------------------------------------------------------------------- 1 | /backend 2 | -------------------------------------------------------------------------------- /D1/.gitignore: -------------------------------------------------------------------------------- 1 | /backend 2 | -------------------------------------------------------------------------------- /B1/static/js/qrcodejs/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | .idea 4 | .project 5 | -------------------------------------------------------------------------------- /B3/static/js/qrcodejs/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | .idea 4 | .project 5 | -------------------------------------------------------------------------------- /C1/static/js/qrcodejs/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | .idea 4 | .project 5 | -------------------------------------------------------------------------------- /D1/static/js/qrcodejs/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | .idea 4 | .project 5 | -------------------------------------------------------------------------------- /A00/web/static/js/qrcodejs/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | .idea 4 | .project 5 | -------------------------------------------------------------------------------- /B2/frontend/public/js/qrcodejs/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | .idea 4 | .project 5 | -------------------------------------------------------------------------------- /template/web/static/js/qrcodejs/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | .idea 4 | .project 5 | -------------------------------------------------------------------------------- /B2/frontend/.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "default": "hot-maze" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /B1/static/img/empty_QR.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Deleplace/hot-maze/HEAD/B1/static/img/empty_QR.png -------------------------------------------------------------------------------- /B3/static/img/empty_QR.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Deleplace/hot-maze/HEAD/B3/static/img/empty_QR.png -------------------------------------------------------------------------------- /C1/static/img/empty_QR.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Deleplace/hot-maze/HEAD/C1/static/img/empty_QR.png -------------------------------------------------------------------------------- /D1/static/img/empty_QR.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Deleplace/hot-maze/HEAD/D1/static/img/empty_QR.png -------------------------------------------------------------------------------- /B1/static/img/hotmaze_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Deleplace/hot-maze/HEAD/B1/static/img/hotmaze_128.png -------------------------------------------------------------------------------- /B1/static/img/red_spinner.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Deleplace/hot-maze/HEAD/B1/static/img/red_spinner.gif -------------------------------------------------------------------------------- /B1/static/img/schema_hot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Deleplace/hot-maze/HEAD/B1/static/img/schema_hot.png -------------------------------------------------------------------------------- /B3/static/img/hotmaze_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Deleplace/hot-maze/HEAD/B3/static/img/hotmaze_128.png -------------------------------------------------------------------------------- /B3/static/img/red_spinner.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Deleplace/hot-maze/HEAD/B3/static/img/red_spinner.gif -------------------------------------------------------------------------------- /B3/static/img/schema_hot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Deleplace/hot-maze/HEAD/B3/static/img/schema_hot.png -------------------------------------------------------------------------------- /C1/static/img/hotmaze_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Deleplace/hot-maze/HEAD/C1/static/img/hotmaze_128.png -------------------------------------------------------------------------------- /C1/static/img/red_spinner.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Deleplace/hot-maze/HEAD/C1/static/img/red_spinner.gif -------------------------------------------------------------------------------- /C1/static/img/schema_hot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Deleplace/hot-maze/HEAD/C1/static/img/schema_hot.png -------------------------------------------------------------------------------- /D1/static/img/hotmaze_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Deleplace/hot-maze/HEAD/D1/static/img/hotmaze_128.png -------------------------------------------------------------------------------- /D1/static/img/red_spinner.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Deleplace/hot-maze/HEAD/D1/static/img/red_spinner.gif -------------------------------------------------------------------------------- /D1/static/img/schema_hot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Deleplace/hot-maze/HEAD/D1/static/img/schema_hot.png -------------------------------------------------------------------------------- /A00/web/static/img/empty_QR.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Deleplace/hot-maze/HEAD/A00/web/static/img/empty_QR.png -------------------------------------------------------------------------------- /A00/web/static/img/hotmaze_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Deleplace/hot-maze/HEAD/A00/web/static/img/hotmaze_128.png -------------------------------------------------------------------------------- /A00/web/static/img/red_spinner.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Deleplace/hot-maze/HEAD/A00/web/static/img/red_spinner.gif -------------------------------------------------------------------------------- /A00/web/static/img/schema_hot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Deleplace/hot-maze/HEAD/A00/web/static/img/schema_hot.png -------------------------------------------------------------------------------- /B1/static/img/transfer_to_cloud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Deleplace/hot-maze/HEAD/B1/static/img/transfer_to_cloud.png -------------------------------------------------------------------------------- /B2/frontend/public/img/empty_QR.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Deleplace/hot-maze/HEAD/B2/frontend/public/img/empty_QR.png -------------------------------------------------------------------------------- /B2/frontend/public/img/schema_hot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Deleplace/hot-maze/HEAD/B2/frontend/public/img/schema_hot.png -------------------------------------------------------------------------------- /B3/static/img/transfer_to_cloud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Deleplace/hot-maze/HEAD/B3/static/img/transfer_to_cloud.png -------------------------------------------------------------------------------- /C1/static/img/transfer_to_cloud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Deleplace/hot-maze/HEAD/C1/static/img/transfer_to_cloud.png -------------------------------------------------------------------------------- /D1/static/img/transfer_to_cloud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Deleplace/hot-maze/HEAD/D1/static/img/transfer_to_cloud.png -------------------------------------------------------------------------------- /template/web/static/img/empty_QR.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Deleplace/hot-maze/HEAD/template/web/static/img/empty_QR.png -------------------------------------------------------------------------------- /B1/static/img/empty_QR_transfering.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Deleplace/hot-maze/HEAD/B1/static/img/empty_QR_transfering.png -------------------------------------------------------------------------------- /B2/frontend/public/img/hotmaze_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Deleplace/hot-maze/HEAD/B2/frontend/public/img/hotmaze_128.png -------------------------------------------------------------------------------- /B2/frontend/public/img/red_spinner.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Deleplace/hot-maze/HEAD/B2/frontend/public/img/red_spinner.gif -------------------------------------------------------------------------------- /B3/static/img/empty_QR_transfering.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Deleplace/hot-maze/HEAD/B3/static/img/empty_QR_transfering.png -------------------------------------------------------------------------------- /C1/static/img/empty_QR_transfering.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Deleplace/hot-maze/HEAD/C1/static/img/empty_QR_transfering.png -------------------------------------------------------------------------------- /D1/static/img/empty_QR_transfering.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Deleplace/hot-maze/HEAD/D1/static/img/empty_QR_transfering.png -------------------------------------------------------------------------------- /template/web/static/img/hotmaze_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Deleplace/hot-maze/HEAD/template/web/static/img/hotmaze_128.png -------------------------------------------------------------------------------- /template/web/static/img/red_spinner.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Deleplace/hot-maze/HEAD/template/web/static/img/red_spinner.gif -------------------------------------------------------------------------------- /template/web/static/img/schema_hot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Deleplace/hot-maze/HEAD/template/web/static/img/schema_hot.png -------------------------------------------------------------------------------- /A00/web/static/img/transfer_to_cloud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Deleplace/hot-maze/HEAD/A00/web/static/img/transfer_to_cloud.png -------------------------------------------------------------------------------- /A00/web/static/img/empty_QR_transfering.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Deleplace/hot-maze/HEAD/A00/web/static/img/empty_QR_transfering.png -------------------------------------------------------------------------------- /B2/frontend/public/img/transfer_to_cloud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Deleplace/hot-maze/HEAD/B2/frontend/public/img/transfer_to_cloud.png -------------------------------------------------------------------------------- /C1/app.yaml: -------------------------------------------------------------------------------- 1 | runtime: go114 2 | 3 | main: ./cmd/backend 4 | 5 | handlers: 6 | - url: /.* 7 | script: auto 8 | secure: always -------------------------------------------------------------------------------- /D1/app.yaml: -------------------------------------------------------------------------------- 1 | runtime: go114 2 | 3 | main: ./cmd/backend 4 | 5 | handlers: 6 | - url: /.* 7 | script: auto 8 | secure: always -------------------------------------------------------------------------------- /B2/frontend/public/img/empty_QR_transfering.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Deleplace/hot-maze/HEAD/B2/frontend/public/img/empty_QR_transfering.png -------------------------------------------------------------------------------- /template/web/static/img/transfer_to_cloud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Deleplace/hot-maze/HEAD/template/web/static/img/transfer_to_cloud.png -------------------------------------------------------------------------------- /template/web/static/img/empty_QR_transfering.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Deleplace/hot-maze/HEAD/template/web/static/img/empty_QR_transfering.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Hot Maze 2 | [Hot Maze](https://hotmaze.io/) is a quick computer-to-mobile share facility, through matrix barcode. 3 | 4 | This repo contains (open source): 5 | - Web client 6 | - Backend 7 | 8 | It is derived from a feature of [Cool Maze](https://github.com/Bartalog/cool-maze). -------------------------------------------------------------------------------- /B1/app.yaml: -------------------------------------------------------------------------------- 1 | runtime: go114 2 | 3 | main: ./cmd/backend 4 | 5 | handlers: 6 | - url: / 7 | static_files: static/index.html 8 | upload: static/index\.html 9 | 10 | - url: /static 11 | static_dir: static 12 | 13 | - url: /.* 14 | script: auto 15 | secure: always 16 | -------------------------------------------------------------------------------- /B2/frontend/firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "hosting": { 3 | "public": "public", 4 | "ignore": [ 5 | "firebase.json", 6 | "**/.*", 7 | "**/node_modules/**" 8 | ] 9 | }, 10 | 11 | "emulators": { 12 | "hosting": { 13 | "port": "8081" 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /B3/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian:buster-slim 2 | RUN set -x && \ 3 | apt-get update && \ 4 | DEBIAN_FRONTEND=noninteractive apt-get install -y \ 5 | ca-certificates && \ 6 | rm -rf /var/lib/apt/lists/* 7 | 8 | 9 | COPY ./server / 10 | COPY ./static /static 11 | 12 | CMD /server 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /D1/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/Deleplace/hot-maze/D1 2 | 3 | go 1.14 4 | 5 | require ( 6 | cloud.google.com/go v0.66.0 7 | cloud.google.com/go/firestore v1.3.0 8 | github.com/google/uuid v1.1.2 9 | google.golang.org/genproto v0.0.0-20200914193844-75d14daec038 10 | google.golang.org/grpc v1.31.1 11 | google.golang.org/protobuf v1.25.0 12 | ) 13 | -------------------------------------------------------------------------------- /B3/config/bucket_cors.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "method": [ 4 | "GET", 5 | "PUT" 6 | ], 7 | "origin": [ 8 | "https://hotmaze.io", 9 | "https://hot-maze-2-3e5dbjxtxq-uc.a.run.app", 10 | "http://localhost:8080", 11 | "http://localhost:8081" 12 | ], 13 | "responseHeader": [ 14 | "Content-Type", 15 | "Content-Disposition" 16 | ] 17 | } 18 | ] 19 | -------------------------------------------------------------------------------- /B2/backend/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/Deleplace/hot-maze/B2 2 | 3 | go 1.14 4 | 5 | require ( 6 | cloud.google.com/go v0.66.0 7 | cloud.google.com/go/storage v1.11.0 8 | github.com/GoogleCloudPlatform/functions-framework-go v1.1.0 9 | github.com/google/uuid v1.1.2 10 | golang.org/x/net v0.0.0-20200904194848-62affa334b73 11 | google.golang.org/genproto v0.0.0-20200921165018-b9da36f5f452 12 | google.golang.org/protobuf v1.25.0 13 | ) 14 | -------------------------------------------------------------------------------- /C1/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/Deleplace/hot-maze/C1 2 | 3 | go 1.14 4 | 5 | require ( 6 | cloud.google.com/go v0.66.0 7 | cloud.google.com/go/firestore v1.3.0 8 | cloud.google.com/go/storage v1.12.0 9 | github.com/google/uuid v1.1.2 10 | golang.org/x/net v0.0.0-20200927032502-5d4f70055728 11 | google.golang.org/genproto v0.0.0-20200925023002-c2d885f95484 12 | google.golang.org/grpc v1.32.0 13 | google.golang.org/protobuf v1.25.0 14 | ) 15 | -------------------------------------------------------------------------------- /B3/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/Deleplace/hot-maze/B3 2 | 3 | go 1.14 4 | 5 | require ( 6 | cloud.google.com/go v0.66.0 7 | cloud.google.com/go/storage v1.12.0 8 | github.com/google/uuid v1.1.2 9 | github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect 10 | golang.org/x/net v0.0.0-20200925080053-05aa5d4ee321 11 | golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43 12 | google.golang.org/genproto v0.0.0-20200925023002-c2d885f95484 13 | google.golang.org/protobuf v1.25.0 14 | ) 15 | -------------------------------------------------------------------------------- /B1/static/js/qrcodejs/bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "qrcode.js", 3 | "version": "0.0.1", 4 | "homepage": "https://github.com/davidshimjs/qrcodejs", 5 | "authors": [ 6 | "Sangmin Shim", "Sangmin Shim (http://jaguarjs.com)" 7 | ], 8 | "description": "Cross-browser QRCode generator for javascript", 9 | "main": "qrcode.js", 10 | "ignore": [ 11 | "bower_components", 12 | "node_modules", 13 | "index.html", 14 | "index.svg", 15 | "jquery.min.js", 16 | "qrcode.min.js" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /B3/static/js/qrcodejs/bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "qrcode.js", 3 | "version": "0.0.1", 4 | "homepage": "https://github.com/davidshimjs/qrcodejs", 5 | "authors": [ 6 | "Sangmin Shim", "Sangmin Shim (http://jaguarjs.com)" 7 | ], 8 | "description": "Cross-browser QRCode generator for javascript", 9 | "main": "qrcode.js", 10 | "ignore": [ 11 | "bower_components", 12 | "node_modules", 13 | "index.html", 14 | "index.svg", 15 | "jquery.min.js", 16 | "qrcode.min.js" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /C1/static/js/qrcodejs/bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "qrcode.js", 3 | "version": "0.0.1", 4 | "homepage": "https://github.com/davidshimjs/qrcodejs", 5 | "authors": [ 6 | "Sangmin Shim", "Sangmin Shim (http://jaguarjs.com)" 7 | ], 8 | "description": "Cross-browser QRCode generator for javascript", 9 | "main": "qrcode.js", 10 | "ignore": [ 11 | "bower_components", 12 | "node_modules", 13 | "index.html", 14 | "index.svg", 15 | "jquery.min.js", 16 | "qrcode.min.js" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /D1/static/js/qrcodejs/bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "qrcode.js", 3 | "version": "0.0.1", 4 | "homepage": "https://github.com/davidshimjs/qrcodejs", 5 | "authors": [ 6 | "Sangmin Shim", "Sangmin Shim (http://jaguarjs.com)" 7 | ], 8 | "description": "Cross-browser QRCode generator for javascript", 9 | "main": "qrcode.js", 10 | "ignore": [ 11 | "bower_components", 12 | "node_modules", 13 | "index.html", 14 | "index.svg", 15 | "jquery.min.js", 16 | "qrcode.min.js" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /A00/web/static/js/qrcodejs/bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "qrcode.js", 3 | "version": "0.0.1", 4 | "homepage": "https://github.com/davidshimjs/qrcodejs", 5 | "authors": [ 6 | "Sangmin Shim", "Sangmin Shim (http://jaguarjs.com)" 7 | ], 8 | "description": "Cross-browser QRCode generator for javascript", 9 | "main": "qrcode.js", 10 | "ignore": [ 11 | "bower_components", 12 | "node_modules", 13 | "index.html", 14 | "index.svg", 15 | "jquery.min.js", 16 | "qrcode.min.js" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /B2/frontend/public/js/qrcodejs/bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "qrcode.js", 3 | "version": "0.0.1", 4 | "homepage": "https://github.com/davidshimjs/qrcodejs", 5 | "authors": [ 6 | "Sangmin Shim", "Sangmin Shim (http://jaguarjs.com)" 7 | ], 8 | "description": "Cross-browser QRCode generator for javascript", 9 | "main": "qrcode.js", 10 | "ignore": [ 11 | "bower_components", 12 | "node_modules", 13 | "index.html", 14 | "index.svg", 15 | "jquery.min.js", 16 | "qrcode.min.js" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /template/web/static/js/qrcodejs/bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "qrcode.js", 3 | "version": "0.0.1", 4 | "homepage": "https://github.com/davidshimjs/qrcodejs", 5 | "authors": [ 6 | "Sangmin Shim", "Sangmin Shim (http://jaguarjs.com)" 7 | ], 8 | "description": "Cross-browser QRCode generator for javascript", 9 | "main": "qrcode.js", 10 | "ignore": [ 11 | "bower_components", 12 | "node_modules", 13 | "index.html", 14 | "index.svg", 15 | "jquery.min.js", 16 | "qrcode.min.js" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /B1/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/Deleplace/hot-maze/B1 2 | 3 | go 1.14 4 | 5 | require ( 6 | cloud.google.com/go v0.65.0 7 | cloud.google.com/go/storage v1.11.0 8 | github.com/google/uuid v1.1.2 9 | golang.org/x/net v0.0.0-20200904194848-62affa334b73 10 | golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43 // indirect 11 | golang.org/x/sys v0.0.0-20200915050820-6d893a6b696e // indirect 12 | google.golang.org/api v0.31.0 // indirect 13 | google.golang.org/genproto v0.0.0-20200914193844-75d14daec038 14 | google.golang.org/grpc v1.32.0 // indirect 15 | google.golang.org/protobuf v1.25.0 16 | ) 17 | -------------------------------------------------------------------------------- /B1/config/bucket_cors.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "method": [ 4 | "GET", 5 | "PUT" 6 | ], 7 | "origin": [ 8 | "https://hotmaze.io", 9 | "https://hot-maze.uc.r.appspot.com", 10 | "https://b1-dot-hot-maze.uc.r.appspot.com", 11 | "https://b1-large-qr-dot-hot-maze.uc.r.appspot.com", 12 | "https://hot-maze.appspot.com", 13 | "https://b1-dot-hot-maze.appspot.com", 14 | "https://b1-large-qr-dot-hot-maze.appspot.com", 15 | "http://localhost:8080" 16 | ], 17 | "responseHeader": [ 18 | "Content-Type", 19 | "Content-Disposition" 20 | ] 21 | } 22 | ] 23 | -------------------------------------------------------------------------------- /B2/backend/server.go: -------------------------------------------------------------------------------- 1 | package hotmaze 2 | 3 | import ( 4 | "time" 5 | 6 | "cloud.google.com/go/storage" 7 | ) 8 | 9 | // Server encapsulates the Hot Maze backend. 10 | type Server struct { 11 | GCPProjectID string 12 | 13 | BackendBaseURL string 14 | 15 | StorageClient *storage.Client 16 | 17 | // Service account email e.g. "ephemeral-storage@hot-maze.iam.gserviceaccount.com" 18 | StorageAccountID string 19 | 20 | // Secret service account private key (PEM). 21 | // Don't check it in, prefer using Secret Manager. 22 | StoragePrivateKey []byte 23 | 24 | // StorageBucket e.g. "hot-maze.appspot.com" 25 | StorageBucket string 26 | 27 | StorageFileTTL time.Duration 28 | 29 | CloudTasksQueuePath string 30 | } 31 | -------------------------------------------------------------------------------- /B2/backend/config/bucket_cors.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "method": [ 4 | "GET", 5 | "PUT" 6 | ], 7 | "origin": [ 8 | "https://hotmaze.io", 9 | "https://hot-maze.uc.r.appspot.com", 10 | "https://b1-dot-hot-maze.uc.r.appspot.com", 11 | "https://b1-large-qr-dot-hot-maze.uc.r.appspot.com", 12 | "https://hot-maze.appspot.com", 13 | "https://b1-dot-hot-maze.appspot.com", 14 | "https://b1-large-qr-dot-hot-maze.appspot.com", 15 | "https://hot-maze.firebaseapp.com", 16 | "https://hot-maze.web.app", 17 | "http://localhost:8080", 18 | "http://localhost:8081" 19 | ], 20 | "responseHeader": [ 21 | "Content-Type", 22 | "Content-Disposition" 23 | ] 24 | } 25 | ] 26 | -------------------------------------------------------------------------------- /B1/secrets.go: -------------------------------------------------------------------------------- 1 | package hotmaze 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | secretmanager "cloud.google.com/go/secretmanager/apiv1" 8 | secretmanagerpb "google.golang.org/genproto/googleapis/cloud/secretmanager/v1" 9 | ) 10 | 11 | func AccessSecretVersion(name string) ([]byte, error) { 12 | ctx := context.Background() 13 | client, err := secretmanager.NewClient(ctx) 14 | if err != nil { 15 | return nil, fmt.Errorf("failed to create secretmanager client: %v", err) 16 | } 17 | 18 | req := &secretmanagerpb.AccessSecretVersionRequest{ 19 | Name: name, 20 | } 21 | 22 | result, err := client.AccessSecretVersion(ctx, req) 23 | if err != nil { 24 | return nil, fmt.Errorf("failed to access secret version: %v", err) 25 | } 26 | 27 | return result.Payload.Data, nil 28 | } 29 | -------------------------------------------------------------------------------- /B3/cors.go: -------------------------------------------------------------------------------- 1 | package hotmaze 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "strings" 7 | ) 8 | 9 | func (s *Server) accessControlAllowHotMaze(w http.ResponseWriter, r *http.Request) error { 10 | origin := r.Header.Get("Origin") 11 | origin = strings.TrimSpace(origin) 12 | 13 | // Specific hosts. 14 | whiteList := []string{ 15 | "https://hotmaze.io", 16 | "https://hot-maze-2-3e5dbjxtxq-uc.a.run.app", // needed? 17 | s.BackendBaseURL, 18 | // For local testing. 19 | "http://localhost:8080", 20 | "http://localhost:8000", 21 | } 22 | for _, whiteItem := range whiteList { 23 | if origin == whiteItem { 24 | w.Header().Set("Access-Control-Allow-Origin", origin) 25 | return nil 26 | } 27 | } 28 | 29 | return fmt.Errorf("origin %q not in whitelist", origin) 30 | } 31 | -------------------------------------------------------------------------------- /B3/secrets.go: -------------------------------------------------------------------------------- 1 | package hotmaze 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | secretmanager "cloud.google.com/go/secretmanager/apiv1" 8 | secretmanagerpb "google.golang.org/genproto/googleapis/cloud/secretmanager/v1" 9 | ) 10 | 11 | func AccessSecretVersion(name string) ([]byte, error) { 12 | ctx := context.Background() 13 | client, err := secretmanager.NewClient(ctx) 14 | if err != nil { 15 | return nil, fmt.Errorf("failed to create secretmanager client: %v", err) 16 | } 17 | 18 | req := &secretmanagerpb.AccessSecretVersionRequest{ 19 | Name: name, 20 | } 21 | 22 | result, err := client.AccessSecretVersion(ctx, req) 23 | if err != nil { 24 | return nil, fmt.Errorf("failed to access secret version: %v", err) 25 | } 26 | 27 | return result.Payload.Data, nil 28 | } 29 | -------------------------------------------------------------------------------- /B2/backend/secrets.go: -------------------------------------------------------------------------------- 1 | package hotmaze 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | secretmanager "cloud.google.com/go/secretmanager/apiv1" 8 | secretmanagerpb "google.golang.org/genproto/googleapis/cloud/secretmanager/v1" 9 | ) 10 | 11 | func AccessSecretVersion(name string) ([]byte, error) { 12 | ctx := context.Background() 13 | client, err := secretmanager.NewClient(ctx) 14 | if err != nil { 15 | return nil, fmt.Errorf("failed to create secretmanager client: %v", err) 16 | } 17 | 18 | req := &secretmanagerpb.AccessSecretVersionRequest{ 19 | Name: name, 20 | } 21 | 22 | result, err := client.AccessSecretVersion(ctx, req) 23 | if err != nil { 24 | return nil, fmt.Errorf("failed to access secret version: %v", err) 25 | } 26 | 27 | return result.Payload.Data, nil 28 | } 29 | -------------------------------------------------------------------------------- /C1/cors.go: -------------------------------------------------------------------------------- 1 | package hotmaze 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "strings" 7 | ) 8 | 9 | func (s Server) accessControlAllowHotMaze(w http.ResponseWriter, r *http.Request) error { 10 | origin := r.Header.Get("Origin") 11 | origin = strings.TrimSpace(origin) 12 | 13 | // Specific hosts. 14 | whiteList := []string{ 15 | "https://hot-maze.appspot.com", 16 | "https://hot-maze.uc.r.appspot.com", 17 | "https://c1-dot-hot-maze.uc.r.appspot.com", 18 | // For debug. 19 | "http://localhost:8080", 20 | "http://localhost:8000", 21 | } 22 | for _, whiteItem := range whiteList { 23 | if origin == whiteItem { 24 | w.Header().Set("Access-Control-Allow-Origin", origin) 25 | return nil 26 | } 27 | } 28 | 29 | return fmt.Errorf("Origin %q not in whitelist", origin) 30 | } 31 | -------------------------------------------------------------------------------- /D1/cors.go: -------------------------------------------------------------------------------- 1 | package hotmaze 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "strings" 7 | ) 8 | 9 | func (s Server) accessControlAllowHotMaze(w http.ResponseWriter, r *http.Request) error { 10 | origin := r.Header.Get("Origin") 11 | origin = strings.TrimSpace(origin) 12 | 13 | // Specific hosts. 14 | whiteList := []string{ 15 | "https://hot-maze.appspot.com", 16 | "https://hot-maze.uc.r.appspot.com", 17 | "https://d1-dot-hot-maze.uc.r.appspot.com", 18 | // For debug. 19 | "http://localhost:8080", 20 | "http://localhost:8000", 21 | } 22 | for _, whiteItem := range whiteList { 23 | if origin == whiteItem { 24 | w.Header().Set("Access-Control-Allow-Origin", origin) 25 | return nil 26 | } 27 | } 28 | 29 | return fmt.Errorf("Origin %q not in whitelist", origin) 30 | } 31 | -------------------------------------------------------------------------------- /B1/.gcloudignore: -------------------------------------------------------------------------------- 1 | # This file specifies files that are *not* uploaded to Google Cloud Platform 2 | # using gcloud. It follows the same syntax as .gitignore, with the addition of 3 | # "#!include" directives (which insert the entries of the given .gitignore-style 4 | # file at that point). 5 | # 6 | # For more information, run: 7 | # $ gcloud topic gcloudignore 8 | # 9 | .gcloudignore 10 | # If you would like to upload your .git directory, .gitignore file or files 11 | # from your .gitignore file, remove the corresponding line 12 | # below: 13 | .git 14 | .gitignore 15 | 16 | # Binaries for programs and plugins 17 | *.exe 18 | *.exe~ 19 | *.dll 20 | *.so 21 | *.dylib 22 | # Test binary, build with `go test -c` 23 | *.test 24 | # Output of the go coverage tool, specifically when used with LiteIDE 25 | *.out -------------------------------------------------------------------------------- /B2/.gcloudignore: -------------------------------------------------------------------------------- 1 | # This file specifies files that are *not* uploaded to Google Cloud Platform 2 | # using gcloud. It follows the same syntax as .gitignore, with the addition of 3 | # "#!include" directives (which insert the entries of the given .gitignore-style 4 | # file at that point). 5 | # 6 | # For more information, run: 7 | # $ gcloud topic gcloudignore 8 | # 9 | .gcloudignore 10 | # If you would like to upload your .git directory, .gitignore file or files 11 | # from your .gitignore file, remove the corresponding line 12 | # below: 13 | .git 14 | .gitignore 15 | 16 | # Binaries for programs and plugins 17 | *.exe 18 | *.exe~ 19 | *.dll 20 | *.so 21 | *.dylib 22 | # Test binary, build with `go test -c` 23 | *.test 24 | # Output of the go coverage tool, specifically when used with LiteIDE 25 | *.out -------------------------------------------------------------------------------- /B3/.gcloudignore: -------------------------------------------------------------------------------- 1 | # This file specifies files that are *not* uploaded to Google Cloud Platform 2 | # using gcloud. It follows the same syntax as .gitignore, with the addition of 3 | # "#!include" directives (which insert the entries of the given .gitignore-style 4 | # file at that point). 5 | # 6 | # For more information, run: 7 | # $ gcloud topic gcloudignore 8 | # 9 | .gcloudignore 10 | # If you would like to upload your .git directory, .gitignore file or files 11 | # from your .gitignore file, remove the corresponding line 12 | # below: 13 | .git 14 | .gitignore 15 | 16 | # Binaries for programs and plugins 17 | *.exe 18 | *.exe~ 19 | *.dll 20 | *.so 21 | *.dylib 22 | # Test binary, build with `go test -c` 23 | *.test 24 | # Output of the go coverage tool, specifically when used with LiteIDE 25 | *.out -------------------------------------------------------------------------------- /C1/.gcloudignore: -------------------------------------------------------------------------------- 1 | # This file specifies files that are *not* uploaded to Google Cloud Platform 2 | # using gcloud. It follows the same syntax as .gitignore, with the addition of 3 | # "#!include" directives (which insert the entries of the given .gitignore-style 4 | # file at that point). 5 | # 6 | # For more information, run: 7 | # $ gcloud topic gcloudignore 8 | # 9 | .gcloudignore 10 | # If you would like to upload your .git directory, .gitignore file or files 11 | # from your .gitignore file, remove the corresponding line 12 | # below: 13 | .git 14 | .gitignore 15 | 16 | # Binaries for programs and plugins 17 | *.exe 18 | *.exe~ 19 | *.dll 20 | *.so 21 | *.dylib 22 | # Test binary, build with `go test -c` 23 | *.test 24 | # Output of the go coverage tool, specifically when used with LiteIDE 25 | *.out -------------------------------------------------------------------------------- /D1/.gcloudignore: -------------------------------------------------------------------------------- 1 | # This file specifies files that are *not* uploaded to Google Cloud Platform 2 | # using gcloud. It follows the same syntax as .gitignore, with the addition of 3 | # "#!include" directives (which insert the entries of the given .gitignore-style 4 | # file at that point). 5 | # 6 | # For more information, run: 7 | # $ gcloud topic gcloudignore 8 | # 9 | .gcloudignore 10 | # If you would like to upload your .git directory, .gitignore file or files 11 | # from your .gitignore file, remove the corresponding line 12 | # below: 13 | .git 14 | .gitignore 15 | 16 | # Binaries for programs and plugins 17 | *.exe 18 | *.exe~ 19 | *.dll 20 | *.so 21 | *.dylib 22 | # Test binary, build with `go test -c` 23 | *.test 24 | # Output of the go coverage tool, specifically when used with LiteIDE 25 | *.out -------------------------------------------------------------------------------- /B1/cors.go: -------------------------------------------------------------------------------- 1 | package hotmaze 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "strings" 7 | ) 8 | 9 | func (s Server) accessControlAllowHotMaze(w http.ResponseWriter, r *http.Request) error { 10 | origin := r.Header.Get("Origin") 11 | origin = strings.TrimSpace(origin) 12 | 13 | // Specific hosts. 14 | whiteList := []string{ 15 | "https://hot-maze.appspot.com", 16 | "https://hot-maze.uc.r.appspot.com", 17 | "https://b1-dot-hot-maze.uc.r.appspot.com", 18 | // For local testing. 19 | "http://localhost:8080", 20 | "http://localhost:8000", 21 | } 22 | for _, whiteItem := range whiteList { 23 | if origin == whiteItem { 24 | w.Header().Set("Access-Control-Allow-Origin", origin) 25 | return nil 26 | } 27 | } 28 | 29 | return fmt.Errorf("Origin %q not in whitelist", origin) 30 | } 31 | -------------------------------------------------------------------------------- /B2/backend/cors.go: -------------------------------------------------------------------------------- 1 | package hotmaze 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "strings" 7 | ) 8 | 9 | // CORS 10 | func (s Server) accessControlAllowHotMaze(w http.ResponseWriter, r *http.Request) error { 11 | origin := r.Header.Get("Origin") 12 | origin = strings.TrimSpace(origin) 13 | 14 | // Specific hosts. 15 | whiteList := []string{ 16 | "https://hotmaze.io", 17 | "https://hot-maze.firebaseapp.com", 18 | "https://hot-maze.web.app", 19 | // For local testing. 20 | "http://localhost:8080", 21 | "http://localhost:8081", 22 | "", 23 | } 24 | for _, whiteItem := range whiteList { 25 | if origin == whiteItem { 26 | w.Header().Set("Access-Control-Allow-Origin", origin) 27 | return nil 28 | } 29 | } 30 | 31 | return fmt.Errorf("Origin %q not in whitelist", origin) 32 | } 33 | -------------------------------------------------------------------------------- /C1/download.go: -------------------------------------------------------------------------------- 1 | package hotmaze 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | "strings" 7 | 8 | "google.golang.org/grpc/codes" 9 | "google.golang.org/grpc/status" 10 | ) 11 | 12 | // HandlerDownload reads file data from Firestore and writes it to w. 13 | func (s Server) HandlerDownload(w http.ResponseWriter, r *http.Request) { 14 | fileUUID := strings.TrimPrefix(r.URL.Path, "/get/") 15 | log.Printf("Reading file %q\n", fileUUID) 16 | 17 | doc, errGet := s.FirestoreClient.Doc("C1/" + fileUUID).Get(r.Context()) 18 | if status.Code(errGet) == codes.NotFound { 19 | log.Println("C1/" + fileUUID + " not found") 20 | http.Error(w, "Resource not found", http.StatusNotFound) 21 | return 22 | } 23 | if errGet != nil { 24 | log.Println(errGet) 25 | http.Error(w, "Problem reading from Firestore :(", http.StatusInternalServerError) 26 | return 27 | } 28 | 29 | fields := doc.Data() 30 | if ct, ok := fields["type"].(string); ok { 31 | w.Header().Set("Content-Type", ct) 32 | } 33 | if fileContents, ok := fields["data"].([]byte); ok { 34 | _, _ = w.Write(fileContents) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /B1/static/js/qrcodejs/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | --------------------- 3 | Copyright (c) 2012 davidshimjs 4 | 5 | Permission is hereby granted, free of charge, 6 | to any person obtaining a copy of this software and associated documentation files (the "Software"), 7 | to deal in the Software without restriction, 8 | including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, 9 | and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /B3/static/js/qrcodejs/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | --------------------- 3 | Copyright (c) 2012 davidshimjs 4 | 5 | Permission is hereby granted, free of charge, 6 | to any person obtaining a copy of this software and associated documentation files (the "Software"), 7 | to deal in the Software without restriction, 8 | including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, 9 | and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /C1/static/js/qrcodejs/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | --------------------- 3 | Copyright (c) 2012 davidshimjs 4 | 5 | Permission is hereby granted, free of charge, 6 | to any person obtaining a copy of this software and associated documentation files (the "Software"), 7 | to deal in the Software without restriction, 8 | including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, 9 | and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /D1/static/js/qrcodejs/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | --------------------- 3 | Copyright (c) 2012 davidshimjs 4 | 5 | Permission is hereby granted, free of charge, 6 | to any person obtaining a copy of this software and associated documentation files (the "Software"), 7 | to deal in the Software without restriction, 8 | including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, 9 | and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /A00/web/static/js/qrcodejs/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | --------------------- 3 | Copyright (c) 2012 davidshimjs 4 | 5 | Permission is hereby granted, free of charge, 6 | to any person obtaining a copy of this software and associated documentation files (the "Software"), 7 | to deal in the Software without restriction, 8 | including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, 9 | and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /B2/frontend/public/js/qrcodejs/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | --------------------- 3 | Copyright (c) 2012 davidshimjs 4 | 5 | Permission is hereby granted, free of charge, 6 | to any person obtaining a copy of this software and associated documentation files (the "Software"), 7 | to deal in the Software without restriction, 8 | including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, 9 | and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /template/web/static/js/qrcodejs/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | --------------------- 3 | Copyright (c) 2012 davidshimjs 4 | 5 | Permission is hereby granted, free of charge, 6 | to any person obtaining a copy of this software and associated documentation files (the "Software"), 7 | to deal in the Software without restriction, 8 | including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, 9 | and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /C1/server.go: -------------------------------------------------------------------------------- 1 | package hotmaze 2 | 3 | import ( 4 | "net/http" 5 | "time" 6 | 7 | cloudtasks "cloud.google.com/go/cloudtasks/apiv2" 8 | "cloud.google.com/go/firestore" 9 | ) 10 | 11 | // Server encapsulates the Hot Maze backend. 12 | type Server struct { 13 | GCPProjectID string 14 | 15 | BackendBaseURL string 16 | 17 | FirestoreClient *firestore.Client 18 | 19 | TasksClient *cloudtasks.Client 20 | 21 | StorageFileTTL time.Duration 22 | 23 | CloudTasksQueuePath string 24 | } 25 | 26 | // RegisterHandlers registers the handlers 27 | func (s Server) RegisterHandlers() { 28 | // Static assets: HTML, JS, CSS 29 | http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 30 | http.ServeFile(w, r, "static/index.html") 31 | }) 32 | http.HandleFunc("/terms.html", func(w http.ResponseWriter, r *http.Request) { 33 | http.ServeFile(w, r, "static/terms.html") 34 | }) 35 | http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("static")))) 36 | 37 | // Backend logic 38 | http.HandleFunc("/upload", s.HandlerUpload) 39 | http.HandleFunc("/get/", s.HandlerDownload) 40 | http.HandleFunc("/forget", s.HandlerForgetFile) 41 | } 42 | -------------------------------------------------------------------------------- /D1/server.go: -------------------------------------------------------------------------------- 1 | package hotmaze 2 | 3 | import ( 4 | "net/http" 5 | "time" 6 | 7 | cloudtasks "cloud.google.com/go/cloudtasks/apiv2" 8 | "cloud.google.com/go/firestore" 9 | ) 10 | 11 | // Server encapsulates the Hot Maze backend. 12 | type Server struct { 13 | GCPProjectID string 14 | 15 | BackendBaseURL string 16 | 17 | FirestoreClient *firestore.Client 18 | 19 | TasksClient *cloudtasks.Client 20 | 21 | StorageFileTTL time.Duration 22 | 23 | CloudTasksQueuePath string 24 | } 25 | 26 | // RegisterHandlers registers the handlers 27 | func (s Server) RegisterHandlers() { 28 | // Static assets: HTML, JS, CSS 29 | http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 30 | http.ServeFile(w, r, "static/index.html") 31 | }) 32 | http.HandleFunc("/terms.html", func(w http.ResponseWriter, r *http.Request) { 33 | http.ServeFile(w, r, "static/terms.html") 34 | }) 35 | http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("static")))) 36 | 37 | // Backend logic 38 | http.HandleFunc("/upload", s.HandlerUpload) 39 | http.HandleFunc("/get/", s.HandlerDownload) 40 | http.HandleFunc("/forget", s.HandlerForgetFile) 41 | } 42 | -------------------------------------------------------------------------------- /B2/backend/cmd/backend/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | 8 | "context" 9 | 10 | hotmaze "github.com/Deleplace/hot-maze/B2" 11 | "github.com/GoogleCloudPlatform/functions-framework-go/funcframework" 12 | ) 13 | 14 | func main() { 15 | // Use PORT environment variable, or default to 8080. 16 | port := "8080" 17 | if envPort := os.Getenv("PORT"); envPort != "" { 18 | port = envPort 19 | } 20 | 21 | hotmaze.GetServer().BackendBaseURL = fmt.Sprintf("http://localhost:%s", port) 22 | 23 | ctx := context.Background() 24 | if err := funcframework.RegisterHTTPFunctionContext(ctx, "/B2_SecureURLs", hotmaze.B2_SecureURLs); err != nil { 25 | log.Fatalf("funcframework.RegisterHTTPFunctionContext: %v\n", err) 26 | } 27 | if err := funcframework.RegisterHTTPFunctionContext(ctx, "/B2_Get/", hotmaze.B2_Get); err != nil { 28 | log.Fatalf("funcframework.RegisterHTTPFunctionContext: %v\n", err) 29 | } 30 | if err := funcframework.RegisterHTTPFunctionContext(ctx, "/B2_Forget", hotmaze.B2_Forget); err != nil { 31 | log.Fatalf("funcframework.RegisterHTTPFunctionContext: %v\n", err) 32 | } 33 | 34 | if err := funcframework.Start(port); err != nil { 35 | log.Fatalf("funcframework.Start: %v\n", err) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /B1/static/js/qrcodejs/index.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |
7 | 8 |
9 | 10 | 35 | 36 |
37 |
38 | -------------------------------------------------------------------------------- /B3/static/js/qrcodejs/index.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |
7 | 8 |
9 | 10 | 35 | 36 |
37 |
38 | -------------------------------------------------------------------------------- /C1/static/js/qrcodejs/index.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |
7 | 8 |
9 | 10 | 35 | 36 |
37 |
38 | -------------------------------------------------------------------------------- /D1/static/js/qrcodejs/index.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |
7 | 8 |
9 | 10 | 35 | 36 |
37 |
38 | -------------------------------------------------------------------------------- /A00/web/static/js/qrcodejs/index.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |
7 | 8 |
9 | 10 | 35 | 36 |
37 |
38 | -------------------------------------------------------------------------------- /B2/frontend/public/js/qrcodejs/index.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |
7 | 8 |
9 | 10 | 35 | 36 |
37 |
38 | -------------------------------------------------------------------------------- /template/web/static/js/qrcodejs/index.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |
7 | 8 |
9 | 10 | 35 | 36 |
37 |
38 | -------------------------------------------------------------------------------- /B1/static/js/qrcodejs/README.md: -------------------------------------------------------------------------------- 1 | # QRCode.js 2 | QRCode.js is javascript library for making QRCode. QRCode.js supports Cross-browser with HTML5 Canvas and table tag in DOM. 3 | QRCode.js has no dependencies. 4 | 5 | ## Basic Usages 6 | ``` 7 |
8 | 11 | ``` 12 | 13 | or with some options 14 | 15 | ``` 16 |
17 | 27 | ``` 28 | 29 | and you can use some methods 30 | 31 | ``` 32 | qrcode.clear(); // clear the code. 33 | qrcode.makeCode("http://naver.com"); // make another code. 34 | ``` 35 | 36 | ## Browser Compatibility 37 | IE6~10, Chrome, Firefox, Safari, Opera, Mobile Safari, Android, Windows Mobile, ETC. 38 | 39 | ## License 40 | MIT License 41 | 42 | ## Contact 43 | twitter @davidshimjs 44 | 45 | [![Bitdeli Badge](https://d2weczhvl823v0.cloudfront.net/davidshimjs/qrcodejs/trend.png)](https://bitdeli.com/free "Bitdeli Badge") 46 | 47 | -------------------------------------------------------------------------------- /B3/static/js/qrcodejs/README.md: -------------------------------------------------------------------------------- 1 | # QRCode.js 2 | QRCode.js is javascript library for making QRCode. QRCode.js supports Cross-browser with HTML5 Canvas and table tag in DOM. 3 | QRCode.js has no dependencies. 4 | 5 | ## Basic Usages 6 | ``` 7 |
8 | 11 | ``` 12 | 13 | or with some options 14 | 15 | ``` 16 |
17 | 27 | ``` 28 | 29 | and you can use some methods 30 | 31 | ``` 32 | qrcode.clear(); // clear the code. 33 | qrcode.makeCode("http://naver.com"); // make another code. 34 | ``` 35 | 36 | ## Browser Compatibility 37 | IE6~10, Chrome, Firefox, Safari, Opera, Mobile Safari, Android, Windows Mobile, ETC. 38 | 39 | ## License 40 | MIT License 41 | 42 | ## Contact 43 | twitter @davidshimjs 44 | 45 | [![Bitdeli Badge](https://d2weczhvl823v0.cloudfront.net/davidshimjs/qrcodejs/trend.png)](https://bitdeli.com/free "Bitdeli Badge") 46 | 47 | -------------------------------------------------------------------------------- /C1/static/js/qrcodejs/README.md: -------------------------------------------------------------------------------- 1 | # QRCode.js 2 | QRCode.js is javascript library for making QRCode. QRCode.js supports Cross-browser with HTML5 Canvas and table tag in DOM. 3 | QRCode.js has no dependencies. 4 | 5 | ## Basic Usages 6 | ``` 7 |
8 | 11 | ``` 12 | 13 | or with some options 14 | 15 | ``` 16 |
17 | 27 | ``` 28 | 29 | and you can use some methods 30 | 31 | ``` 32 | qrcode.clear(); // clear the code. 33 | qrcode.makeCode("http://naver.com"); // make another code. 34 | ``` 35 | 36 | ## Browser Compatibility 37 | IE6~10, Chrome, Firefox, Safari, Opera, Mobile Safari, Android, Windows Mobile, ETC. 38 | 39 | ## License 40 | MIT License 41 | 42 | ## Contact 43 | twitter @davidshimjs 44 | 45 | [![Bitdeli Badge](https://d2weczhvl823v0.cloudfront.net/davidshimjs/qrcodejs/trend.png)](https://bitdeli.com/free "Bitdeli Badge") 46 | 47 | -------------------------------------------------------------------------------- /D1/static/js/qrcodejs/README.md: -------------------------------------------------------------------------------- 1 | # QRCode.js 2 | QRCode.js is javascript library for making QRCode. QRCode.js supports Cross-browser with HTML5 Canvas and table tag in DOM. 3 | QRCode.js has no dependencies. 4 | 5 | ## Basic Usages 6 | ``` 7 |
8 | 11 | ``` 12 | 13 | or with some options 14 | 15 | ``` 16 |
17 | 27 | ``` 28 | 29 | and you can use some methods 30 | 31 | ``` 32 | qrcode.clear(); // clear the code. 33 | qrcode.makeCode("http://naver.com"); // make another code. 34 | ``` 35 | 36 | ## Browser Compatibility 37 | IE6~10, Chrome, Firefox, Safari, Opera, Mobile Safari, Android, Windows Mobile, ETC. 38 | 39 | ## License 40 | MIT License 41 | 42 | ## Contact 43 | twitter @davidshimjs 44 | 45 | [![Bitdeli Badge](https://d2weczhvl823v0.cloudfront.net/davidshimjs/qrcodejs/trend.png)](https://bitdeli.com/free "Bitdeli Badge") 46 | 47 | -------------------------------------------------------------------------------- /A00/web/static/js/qrcodejs/README.md: -------------------------------------------------------------------------------- 1 | # QRCode.js 2 | QRCode.js is javascript library for making QRCode. QRCode.js supports Cross-browser with HTML5 Canvas and table tag in DOM. 3 | QRCode.js has no dependencies. 4 | 5 | ## Basic Usages 6 | ``` 7 |
8 | 11 | ``` 12 | 13 | or with some options 14 | 15 | ``` 16 |
17 | 27 | ``` 28 | 29 | and you can use some methods 30 | 31 | ``` 32 | qrcode.clear(); // clear the code. 33 | qrcode.makeCode("http://naver.com"); // make another code. 34 | ``` 35 | 36 | ## Browser Compatibility 37 | IE6~10, Chrome, Firefox, Safari, Opera, Mobile Safari, Android, Windows Mobile, ETC. 38 | 39 | ## License 40 | MIT License 41 | 42 | ## Contact 43 | twitter @davidshimjs 44 | 45 | [![Bitdeli Badge](https://d2weczhvl823v0.cloudfront.net/davidshimjs/qrcodejs/trend.png)](https://bitdeli.com/free "Bitdeli Badge") 46 | 47 | -------------------------------------------------------------------------------- /B2/frontend/public/js/qrcodejs/README.md: -------------------------------------------------------------------------------- 1 | # QRCode.js 2 | QRCode.js is javascript library for making QRCode. QRCode.js supports Cross-browser with HTML5 Canvas and table tag in DOM. 3 | QRCode.js has no dependencies. 4 | 5 | ## Basic Usages 6 | ``` 7 |
8 | 11 | ``` 12 | 13 | or with some options 14 | 15 | ``` 16 |
17 | 27 | ``` 28 | 29 | and you can use some methods 30 | 31 | ``` 32 | qrcode.clear(); // clear the code. 33 | qrcode.makeCode("http://naver.com"); // make another code. 34 | ``` 35 | 36 | ## Browser Compatibility 37 | IE6~10, Chrome, Firefox, Safari, Opera, Mobile Safari, Android, Windows Mobile, ETC. 38 | 39 | ## License 40 | MIT License 41 | 42 | ## Contact 43 | twitter @davidshimjs 44 | 45 | [![Bitdeli Badge](https://d2weczhvl823v0.cloudfront.net/davidshimjs/qrcodejs/trend.png)](https://bitdeli.com/free "Bitdeli Badge") 46 | 47 | -------------------------------------------------------------------------------- /template/web/static/js/qrcodejs/README.md: -------------------------------------------------------------------------------- 1 | # QRCode.js 2 | QRCode.js is javascript library for making QRCode. QRCode.js supports Cross-browser with HTML5 Canvas and table tag in DOM. 3 | QRCode.js has no dependencies. 4 | 5 | ## Basic Usages 6 | ``` 7 |
8 | 11 | ``` 12 | 13 | or with some options 14 | 15 | ``` 16 |
17 | 27 | ``` 28 | 29 | and you can use some methods 30 | 31 | ``` 32 | qrcode.clear(); // clear the code. 33 | qrcode.makeCode("http://naver.com"); // make another code. 34 | ``` 35 | 36 | ## Browser Compatibility 37 | IE6~10, Chrome, Firefox, Safari, Opera, Mobile Safari, Android, Windows Mobile, ETC. 38 | 39 | ## License 40 | MIT License 41 | 42 | ## Contact 43 | twitter @davidshimjs 44 | 45 | [![Bitdeli Badge](https://d2weczhvl823v0.cloudfront.net/davidshimjs/qrcodejs/trend.png)](https://bitdeli.com/free "Bitdeli Badge") 46 | 47 | -------------------------------------------------------------------------------- /B1/static/js/qrcodejs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Cross-Browser QRCode generator for Javascript 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 |
13 | 14 | 44 | -------------------------------------------------------------------------------- /B3/static/js/qrcodejs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Cross-Browser QRCode generator for Javascript 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 |
13 | 14 | 44 | -------------------------------------------------------------------------------- /C1/static/js/qrcodejs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Cross-Browser QRCode generator for Javascript 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 |
13 | 14 | 44 | -------------------------------------------------------------------------------- /D1/static/js/qrcodejs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Cross-Browser QRCode generator for Javascript 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 |
13 | 14 | 44 | -------------------------------------------------------------------------------- /A00/web/static/js/qrcodejs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Cross-Browser QRCode generator for Javascript 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 |
13 | 14 | 44 | -------------------------------------------------------------------------------- /B2/frontend/public/js/qrcodejs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Cross-Browser QRCode generator for Javascript 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 |
13 | 14 | 44 | -------------------------------------------------------------------------------- /template/web/static/js/qrcodejs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Cross-Browser QRCode generator for Javascript 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 |
13 | 14 | 44 | -------------------------------------------------------------------------------- /B1/server.go: -------------------------------------------------------------------------------- 1 | package hotmaze 2 | 3 | import ( 4 | "net/http" 5 | "time" 6 | 7 | "cloud.google.com/go/storage" 8 | ) 9 | 10 | // Server encapsulates the Hot Maze backend. 11 | type Server struct { 12 | GCPProjectID string 13 | 14 | BackendBaseURL string 15 | 16 | StorageClient *storage.Client 17 | 18 | // Service account email e.g. "ephemeral-storage@hot-maze.iam.gserviceaccount.com" 19 | StorageAccountID string 20 | 21 | // Secret service account private key (PEM). 22 | // Don't check it in, prefer using Secret Manager. 23 | StoragePrivateKey []byte 24 | 25 | // StorageBucket e.g. "hot-maze.appspot.com" 26 | StorageBucket string 27 | 28 | StorageFileTTL time.Duration 29 | 30 | CloudTasksQueuePath string 31 | } 32 | 33 | // RegisterHandlers registers the handlers 34 | func (s Server) RegisterHandlers() { 35 | // Static assets: HTML, JS, CSS 36 | http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 37 | http.ServeFile(w, r, "static/index.html") 38 | }) 39 | http.HandleFunc("/terms.html", func(w http.ResponseWriter, r *http.Request) { 40 | http.ServeFile(w, r, "static/terms.html") 41 | }) 42 | http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("static")))) 43 | 44 | // Backend logic 45 | http.HandleFunc("/secure-urls", s.HandlerGenerateSignedURLs) 46 | http.HandleFunc("/get/", s.HandlerUnshortenGetURL) 47 | http.HandleFunc("/forget", s.HandlerForgetFile) 48 | } 49 | -------------------------------------------------------------------------------- /B2/frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | firebase-debug.log* 8 | firebase-debug.*.log* 9 | 10 | # Firebase cache 11 | .firebase/ 12 | 13 | # Firebase config 14 | 15 | # Uncomment this if you'd like others to create their own Firebase project. 16 | # For a team working on the same Firebase project(s), it is recommended to leave 17 | # it commented so all members can deploy to the same project(s) in .firebaserc. 18 | # .firebaserc 19 | 20 | # Runtime data 21 | pids 22 | *.pid 23 | *.seed 24 | *.pid.lock 25 | 26 | # Directory for instrumented libs generated by jscoverage/JSCover 27 | lib-cov 28 | 29 | # Coverage directory used by tools like istanbul 30 | coverage 31 | 32 | # nyc test coverage 33 | .nyc_output 34 | 35 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 36 | .grunt 37 | 38 | # Bower dependency directory (https://bower.io/) 39 | bower_components 40 | 41 | # node-waf configuration 42 | .lock-wscript 43 | 44 | # Compiled binary addons (http://nodejs.org/api/addons.html) 45 | build/Release 46 | 47 | # Dependency directories 48 | node_modules/ 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Optional REPL history 57 | .node_repl_history 58 | 59 | # Output of 'npm pack' 60 | *.tgz 61 | 62 | # Yarn Integrity file 63 | .yarn-integrity 64 | 65 | # dotenv environment variables file 66 | .env 67 | -------------------------------------------------------------------------------- /B3/server.go: -------------------------------------------------------------------------------- 1 | package hotmaze 2 | 3 | import ( 4 | "net/http" 5 | "time" 6 | 7 | "cloud.google.com/go/storage" 8 | ) 9 | 10 | // Server encapsulates the Hot Maze backend. 11 | type Server struct { 12 | GCPProjectID string 13 | 14 | BackendBaseURL string 15 | 16 | StorageClient *storage.Client 17 | 18 | // Service account email e.g. "ephemeral-storage@hot-maze.iam.gserviceaccount.com" 19 | StorageAccountID string 20 | 21 | // Secret service account private key (PEM). 22 | // Don't check it in, prefer using Secret Manager. 23 | StoragePrivateKey []byte 24 | 25 | // StorageBucket e.g. "hot-maze.appspot.com" 26 | StorageBucket string 27 | 28 | StorageFileTTL time.Duration 29 | 30 | CloudTasksQueuePath string 31 | } 32 | 33 | // RegisterHandlers registers the handlers 34 | func (s *Server) RegisterHandlers() { 35 | // Static assets: HTML, JS, CSS 36 | http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 37 | http.ServeFile(w, r, "static/index.html") 38 | }) 39 | http.HandleFunc("/terms.html", func(w http.ResponseWriter, r *http.Request) { 40 | http.ServeFile(w, r, "static/terms.html") 41 | }) 42 | http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("static")))) 43 | 44 | // Backend logic 45 | http.HandleFunc("/secure-urls", s.HandlerGenerateSignedURLs) 46 | http.HandleFunc("/get/", s.HandlerUnshortenGetURL) 47 | http.HandleFunc("/forget", s.HandlerForgetFile) 48 | http.HandleFunc("/term", s.HandlerDirectUpload) 49 | } 50 | -------------------------------------------------------------------------------- /B1/static/js/qrcodejs/index-svg.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Cross-Browser QRCode generator for Javascript 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /B3/static/js/qrcodejs/index-svg.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Cross-Browser QRCode generator for Javascript 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /C1/static/js/qrcodejs/index-svg.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Cross-Browser QRCode generator for Javascript 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /D1/static/js/qrcodejs/index-svg.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Cross-Browser QRCode generator for Javascript 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /A00/web/static/js/qrcodejs/index-svg.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Cross-Browser QRCode generator for Javascript 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /B2/frontend/public/js/qrcodejs/index-svg.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Cross-Browser QRCode generator for Javascript 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /template/web/static/js/qrcodejs/index-svg.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Cross-Browser QRCode generator for Javascript 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /C1/cmd/backend/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "net/http" 8 | "os" 9 | "time" 10 | 11 | cloudtasks "cloud.google.com/go/cloudtasks/apiv2" 12 | "cloud.google.com/go/firestore" 13 | hotmaze "github.com/Deleplace/hot-maze/C1" 14 | ) 15 | 16 | const ( 17 | projectID = "hot-maze" 18 | // "c1" is the version of this App Engine app 19 | // "uc.r" is the app location (regional route) 20 | backendBaseURL = "https://c1-dot-hot-maze.uc.r.appspot.com" 21 | storageServiceAccountID = "ephemeral-storage@hot-maze.iam.gserviceaccount.com" 22 | bucket = "hot-maze.appspot.com" 23 | fileDeleteAfter = 9 * time.Minute 24 | ) 25 | 26 | func main() { 27 | ctx := context.Background() 28 | 29 | fsClient, err := firestore.NewClient(ctx, projectID) 30 | if err != nil { 31 | log.Fatalf("Problem accessing Firestore: %v\n", err) 32 | } 33 | defer fsClient.Close() 34 | 35 | tasksClient, err := cloudtasks.NewClient(ctx) 36 | if err != nil { 37 | log.Fatalf("Problem accessing Cloud Tasks: %v\n", err) 38 | } 39 | defer tasksClient.Close() 40 | 41 | server := hotmaze.Server{ 42 | GCPProjectID: projectID, 43 | BackendBaseURL: backendBaseURL, 44 | FirestoreClient: fsClient, 45 | TasksClient: tasksClient, 46 | StorageFileTTL: fileDeleteAfter, 47 | CloudTasksQueuePath: "projects/hot-maze/locations/us-central1/queues/c1-file-expiry", 48 | } 49 | server.RegisterHandlers() 50 | 51 | port := os.Getenv("PORT") 52 | if port == "" { 53 | port = "8080" 54 | log.Printf("Defaulting to port %s", port) 55 | } 56 | 57 | log.Printf("Listening on port %s", port) 58 | err = http.ListenAndServe(fmt.Sprintf(":%s", port), nil) 59 | log.Fatal(err) 60 | } 61 | -------------------------------------------------------------------------------- /D1/cmd/backend/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "net/http" 8 | "os" 9 | "time" 10 | 11 | cloudtasks "cloud.google.com/go/cloudtasks/apiv2" 12 | "cloud.google.com/go/firestore" 13 | hotmaze "github.com/Deleplace/hot-maze/D1" 14 | ) 15 | 16 | const ( 17 | projectID = "hot-maze" 18 | // "d1" is the version of this App Engine app 19 | // "uc.r" is the app location (regional route) 20 | backendBaseURL = "https://d1-dot-hot-maze.uc.r.appspot.com" 21 | storageServiceAccountID = "ephemeral-storage@hot-maze.iam.gserviceaccount.com" 22 | bucket = "hot-maze.appspot.com" 23 | fileDeleteAfter = 9 * time.Minute 24 | ) 25 | 26 | func main() { 27 | ctx := context.Background() 28 | 29 | fsClient, err := firestore.NewClient(ctx, projectID) 30 | if err != nil { 31 | log.Fatalf("Problem accessing Firestore: %v\n", err) 32 | } 33 | defer fsClient.Close() 34 | 35 | tasksClient, err := cloudtasks.NewClient(ctx) 36 | if err != nil { 37 | log.Fatalf("Problem accessing Cloud Tasks: %v\n", err) 38 | } 39 | defer tasksClient.Close() 40 | 41 | server := hotmaze.Server{ 42 | GCPProjectID: projectID, 43 | BackendBaseURL: backendBaseURL, 44 | FirestoreClient: fsClient, 45 | TasksClient: tasksClient, 46 | StorageFileTTL: fileDeleteAfter, 47 | CloudTasksQueuePath: "projects/hot-maze/locations/us-central1/queues/d1-file-expiry", 48 | } 49 | server.RegisterHandlers() 50 | 51 | port := os.Getenv("PORT") 52 | if port == "" { 53 | port = "8080" 54 | log.Printf("Defaulting to port %s", port) 55 | } 56 | 57 | log.Printf("Listening on port %s", port) 58 | err = http.ListenAndServe(fmt.Sprintf(":%s", port), nil) 59 | log.Fatal(err) 60 | } 61 | -------------------------------------------------------------------------------- /B1/cmd/backend/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net/http" 7 | "os" 8 | "time" 9 | 10 | "cloud.google.com/go/storage" 11 | hotmaze "github.com/Deleplace/hot-maze/B1" 12 | "golang.org/x/net/context" 13 | ) 14 | 15 | const ( 16 | projectID = "hot-maze" 17 | // "b1" is the version of this App Engine app 18 | // "uc.r" is the app location (regional route) 19 | backendBaseURL = "https://b1-dot-hot-maze.uc.r.appspot.com" 20 | storageServiceAccountID = "ephemeral-storage@hot-maze.iam.gserviceaccount.com" 21 | bucket = "hot-maze.appspot.com" 22 | fileDeleteAfter = 9 * time.Minute 23 | ) 24 | 25 | func main() { 26 | ctx := context.Background() 27 | storageClient, err := storage.NewClient(ctx) 28 | if err != nil { 29 | log.Fatal("Couldn't create Storage client:", err) 30 | } 31 | 32 | storagePrivateKey, errSecret := hotmaze.AccessSecretVersion("projects/230384242501/secrets/B1-storage-private-key/versions/latest") 33 | if errSecret != nil { 34 | log.Fatal("Couldn't read Storage service account private key:", errSecret) 35 | } 36 | 37 | server := hotmaze.Server{ 38 | GCPProjectID: projectID, 39 | BackendBaseURL: backendBaseURL, 40 | StorageClient: storageClient, 41 | StorageAccountID: storageServiceAccountID, 42 | StoragePrivateKey: storagePrivateKey, 43 | StorageBucket: bucket, 44 | StorageFileTTL: fileDeleteAfter, 45 | CloudTasksQueuePath: "projects/hot-maze/locations/us-central1/queues/b1-file-expiry", 46 | } 47 | server.RegisterHandlers() 48 | 49 | port := os.Getenv("PORT") 50 | if port == "" { 51 | port = "8080" 52 | log.Printf("Defaulting to port %s", port) 53 | } 54 | 55 | log.Printf("Listening on port %s", port) 56 | err = http.ListenAndServe(fmt.Sprintf(":%s", port), nil) 57 | log.Fatal(err) 58 | } 59 | -------------------------------------------------------------------------------- /B2/frontend/public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Page Not Found 7 | 8 | 23 | 24 | 25 |
26 |

404

27 |

Page Not Found

28 |

The specified file was not found on this website. Please check the URL for mistakes and try again.

29 |

Why am I seeing this?

30 |

This page was generated by the Firebase Command-Line Interface. To modify it, edit the 404.html file in your project's configured public directory.

31 |
32 | 33 | 34 | -------------------------------------------------------------------------------- /B2/backend/functions.go: -------------------------------------------------------------------------------- 1 | package hotmaze 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "net/http" 7 | "time" 8 | 9 | "cloud.google.com/go/storage" 10 | ) 11 | 12 | // All the configuration & initialization happen here, as they 13 | // are necessary to the Cloud Functions exposed below. 14 | 15 | const ( 16 | projectID = "hot-maze" 17 | backendBaseURL = "https://us-central1-hot-maze.cloudfunctions.net" 18 | storageServiceAccountID = "ephemeral-storage@hot-maze.iam.gserviceaccount.com" 19 | bucket = "hot-maze.appspot.com" 20 | fileDeleteAfter = 9 * time.Minute 21 | ) 22 | 23 | var server Server 24 | 25 | func init() { 26 | ctx := context.Background() 27 | storageClient, err := storage.NewClient(ctx) 28 | if err != nil { 29 | log.Fatal("Couldn't create Storage client:", err) 30 | } 31 | 32 | storagePrivateKey, errSecret := AccessSecretVersion("projects/230384242501/secrets/B2-storage-private-key/versions/latest") 33 | if errSecret != nil { 34 | log.Fatal("Couldn't read Storage service account private key:", errSecret) 35 | } 36 | 37 | server = Server{ 38 | GCPProjectID: projectID, 39 | BackendBaseURL: backendBaseURL, 40 | StorageClient: storageClient, 41 | StorageAccountID: storageServiceAccountID, 42 | StoragePrivateKey: storagePrivateKey, 43 | StorageBucket: bucket, 44 | StorageFileTTL: fileDeleteAfter, 45 | CloudTasksQueuePath: "projects/hot-maze/locations/us-central1/queues/b2-file-expiry", 46 | } 47 | } 48 | 49 | func B2_SecureURLs(w http.ResponseWriter, r *http.Request) { 50 | server.HandlerGenerateSignedURLs(w, r) 51 | } 52 | 53 | func B2_Get(w http.ResponseWriter, r *http.Request) { 54 | server.HandlerUnshortenGetURL(w, r) 55 | } 56 | 57 | func B2_Forget(w http.ResponseWriter, r *http.Request) { 58 | server.HandlerForgetFile(w, r) 59 | } 60 | 61 | // GetServer is exposed to modify the Server settings, 62 | // e.g. for local testing. 63 | func GetServer() *Server { 64 | return &server 65 | } 66 | -------------------------------------------------------------------------------- /C1/upload.go: -------------------------------------------------------------------------------- 1 | package hotmaze 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | "log" 7 | "net/http" 8 | "time" 9 | 10 | "github.com/google/uuid" 11 | ) 12 | 13 | const ( 14 | validity = 300 * time.Second 15 | ) 16 | 17 | func (s Server) HandlerUpload(w http.ResponseWriter, r *http.Request) { 18 | if errCORS := s.accessControlAllowHotMaze(w, r); errCORS != nil { 19 | log.Println(errCORS) 20 | http.Error(w, errCORS.Error(), http.StatusBadRequest) 21 | return 22 | } 23 | 24 | if r.Method != "POST" { 25 | http.Error(w, "POST only", http.StatusBadRequest) 26 | return 27 | } 28 | 29 | fileData, err := ioutil.ReadAll(r.Body) 30 | if err != nil { 31 | http.Error(w, "Could not read file data :(", http.StatusInternalServerError) 32 | return 33 | } 34 | defer r.Body.Close() 35 | if len(fileData) == 0 { 36 | http.Error(w, "Empty file :(", http.StatusBadRequest) 37 | return 38 | } 39 | contentType := r.Header.Get("Content-Type") 40 | log.Println("Received a resource of type", contentType, "and size", len(fileData)) 41 | 42 | fileUUID := uuid.New().String() 43 | log.Println("Saving with UUID", fileUUID) 44 | 45 | _, errCreate := s.FirestoreClient.Doc("C1/"+fileUUID).Create(r.Context(), map[string]interface{}{ 46 | "data": fileData, 47 | "type": contentType, 48 | }) 49 | if errCreate != nil { 50 | log.Println(errCreate) 51 | http.Error(w, "Problem writing to Firestore :(", http.StatusInternalServerError) 52 | return 53 | } 54 | 55 | _, err = s.ScheduleForgetFile(r.Context(), fileUUID) 56 | if err != nil { 57 | log.Println("scheduling file expiry:", err) 58 | // Better fail now, than keeping a user file forever in Firestore 59 | http.Error(w, "Problem with file allocation :(", http.StatusInternalServerError) 60 | return 61 | } 62 | log.Println("File", fileUUID, "is scheduled for deletion") 63 | 64 | w.Header().Set("Content-Type", "application/json") 65 | json.NewEncoder(w).Encode(map[string]interface{}{ 66 | "downloadURL": s.BackendBaseURL + "/get/" + fileUUID, 67 | }) 68 | } 69 | -------------------------------------------------------------------------------- /B3/fileexpiry.go: -------------------------------------------------------------------------------- 1 | package hotmaze 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "net/http" 8 | "time" 9 | 10 | cloudtasks "cloud.google.com/go/cloudtasks/apiv2" 11 | "cloud.google.com/go/storage" 12 | taskspb "google.golang.org/genproto/googleapis/cloud/tasks/v2" 13 | "google.golang.org/protobuf/types/known/timestamppb" 14 | ) 15 | 16 | func (s *Server) HandlerForgetFile(w http.ResponseWriter, r *http.Request) { 17 | if r.Method != "POST" { 18 | http.Error(w, "POST only", http.StatusBadRequest) 19 | return 20 | } 21 | 22 | fileUUID := r.FormValue("uuid") 23 | if fileUUID == "" { 24 | http.Error(w, "please provide file uuid", http.StatusBadRequest) 25 | return 26 | } 27 | 28 | objectName := "transit/" + fileUUID 29 | log.Println("Forgetting file", objectName) 30 | err := s.StorageClient.Bucket(s.StorageBucket).Object(objectName).Delete(r.Context()) 31 | switch { 32 | case err == storage.ErrObjectNotExist: 33 | log.Println("File", objectName, "did not exist") 34 | case err != nil: 35 | http.Error(w, err.Error(), http.StatusInternalServerError) 36 | return 37 | } 38 | } 39 | 40 | func (s *Server) ScheduleForgetFile(ctx context.Context, fileUUID string) (*taskspb.Task, error) { 41 | // Adapted from https://cloud.google.com/tasks/docs/creating-http-target-tasks#go 42 | 43 | client, err := cloudtasks.NewClient(ctx) 44 | if err != nil { 45 | return nil, fmt.Errorf("cloudtasks.NewClient: %v", err) 46 | } 47 | defer client.Close() 48 | 49 | req := &taskspb.CreateTaskRequest{ 50 | Parent: s.CloudTasksQueuePath, 51 | Task: &taskspb.Task{ 52 | MessageType: &taskspb.Task_HttpRequest{ 53 | HttpRequest: &taskspb.HttpRequest{ 54 | HttpMethod: taskspb.HttpMethod_POST, 55 | Url: s.BackendBaseURL + "/forget?uuid=" + fileUUID, 56 | }, 57 | }, 58 | ScheduleTime: ×tamppb.Timestamp{ 59 | Seconds: time.Now().Add(s.StorageFileTTL).Unix(), 60 | }, 61 | }, 62 | } 63 | 64 | createdTask, err := client.CreateTask(ctx, req) 65 | if err != nil { 66 | return nil, fmt.Errorf("CreateTask: %v", err) 67 | } 68 | 69 | return createdTask, nil 70 | } 71 | -------------------------------------------------------------------------------- /B2/backend/fileexpiry.go: -------------------------------------------------------------------------------- 1 | package hotmaze 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "net/http" 8 | "time" 9 | 10 | cloudtasks "cloud.google.com/go/cloudtasks/apiv2" 11 | "cloud.google.com/go/storage" 12 | taskspb "google.golang.org/genproto/googleapis/cloud/tasks/v2" 13 | "google.golang.org/protobuf/types/known/timestamppb" 14 | ) 15 | 16 | func (s Server) HandlerForgetFile(w http.ResponseWriter, r *http.Request) { 17 | if r.Method != "POST" { 18 | http.Error(w, "POST only", http.StatusBadRequest) 19 | return 20 | } 21 | 22 | fileUUID := r.FormValue("uuid") 23 | if fileUUID == "" { 24 | http.Error(w, "please provide file uuid", http.StatusBadRequest) 25 | return 26 | } 27 | 28 | objectName := "transit/" + fileUUID 29 | log.Println("Forgetting file", objectName) 30 | err := s.StorageClient.Bucket(s.StorageBucket).Object(objectName).Delete(r.Context()) 31 | switch { 32 | case err == storage.ErrObjectNotExist: 33 | log.Println("File", objectName, "did not exist") 34 | case err != nil: 35 | http.Error(w, err.Error(), http.StatusInternalServerError) 36 | return 37 | } 38 | } 39 | 40 | func (s Server) ScheduleForgetFile(ctx context.Context, fileUUID string) (*taskspb.Task, error) { 41 | // Adapted from https://cloud.google.com/tasks/docs/creating-http-target-tasks#go 42 | 43 | client, err := cloudtasks.NewClient(ctx) 44 | if err != nil { 45 | return nil, fmt.Errorf("cloudtasks.NewClient: %v", err) 46 | } 47 | defer client.Close() 48 | 49 | // Build the Task payload. 50 | // https://godoc.org/google.golang.org/genproto/googleapis/cloud/tasks/v2#CreateTaskRequest 51 | req := &taskspb.CreateTaskRequest{ 52 | Parent: s.CloudTasksQueuePath, 53 | Task: &taskspb.Task{ 54 | MessageType: &taskspb.Task_HttpRequest{ 55 | HttpRequest: &taskspb.HttpRequest{ 56 | HttpMethod: taskspb.HttpMethod_POST, 57 | Url: s.BackendBaseURL + "/B2_Forget?uuid=" + fileUUID, 58 | }, 59 | }, 60 | ScheduleTime: ×tamppb.Timestamp{ 61 | Seconds: time.Now().Add(s.StorageFileTTL).Unix(), 62 | }, 63 | }, 64 | } 65 | 66 | createdTask, err := client.CreateTask(ctx, req) 67 | if err != nil { 68 | return nil, fmt.Errorf("CreateTask: %v", err) 69 | } 70 | 71 | return createdTask, nil 72 | } 73 | -------------------------------------------------------------------------------- /C1/fileexpiry.go: -------------------------------------------------------------------------------- 1 | package hotmaze 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "net/http" 8 | "time" 9 | 10 | taskspb "google.golang.org/genproto/googleapis/cloud/tasks/v2" 11 | "google.golang.org/protobuf/types/known/timestamppb" 12 | ) 13 | 14 | func (s Server) HandlerForgetFile(w http.ResponseWriter, r *http.Request) { 15 | if r.Method != "POST" { 16 | http.Error(w, "POST only", http.StatusBadRequest) 17 | return 18 | } 19 | 20 | if taskName := r.Header.Get("X-Appengine-Taskname"); taskName == "" { 21 | // Validate the request comes from Cloud Tasks. 22 | log.Println("Invalid Task: No X-Appengine-Taskname request header found") 23 | http.Error(w, "Bad Request - Invalid Task", http.StatusBadRequest) 24 | return 25 | } 26 | 27 | fileUUID := r.FormValue("uuid") 28 | if fileUUID == "" { 29 | http.Error(w, "please provide file uuid", http.StatusBadRequest) 30 | return 31 | } 32 | 33 | log.Println("Deleting resource C1/" + fileUUID + " from Firestore") 34 | _, errDelete := s.FirestoreClient.Doc("C1/" + fileUUID).Delete(r.Context()) 35 | if errDelete != nil { 36 | log.Println(errDelete) 37 | http.Error(w, "Problem deleting a resource from Firestore :(", http.StatusInternalServerError) 38 | return 39 | } 40 | } 41 | 42 | func (s Server) ScheduleForgetFile(ctx context.Context, fileUUID string) (*taskspb.Task, error) { 43 | // Adapted from https://cloud.google.com/tasks/docs/creating-appengine-tasks#go 44 | 45 | // Build the Task payload. 46 | // https://godoc.org/google.golang.org/genproto/googleapis/cloud/tasks/v2#CreateTaskRequest 47 | req := &taskspb.CreateTaskRequest{ 48 | Parent: s.CloudTasksQueuePath, 49 | Task: &taskspb.Task{ 50 | // https://godoc.org/google.golang.org/genproto/googleapis/cloud/tasks/v2#AppEngineHttpRequest 51 | MessageType: &taskspb.Task_AppEngineHttpRequest{ 52 | AppEngineHttpRequest: &taskspb.AppEngineHttpRequest{ 53 | HttpMethod: taskspb.HttpMethod_POST, 54 | RelativeUri: "/forget?uuid=" + fileUUID, 55 | AppEngineRouting: &taskspb.AppEngineRouting{ 56 | Version: "c1", 57 | }, 58 | }, 59 | }, 60 | ScheduleTime: ×tamppb.Timestamp{ 61 | Seconds: time.Now().Add(s.StorageFileTTL).Unix(), 62 | }, 63 | }, 64 | } 65 | 66 | createdTask, err := s.TasksClient.CreateTask(ctx, req) 67 | if err != nil { 68 | return nil, fmt.Errorf("CreateTask: %v", err) 69 | } 70 | 71 | return createdTask, nil 72 | } 73 | -------------------------------------------------------------------------------- /B3/cmd/backend/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net/http" 7 | "os" 8 | "time" 9 | 10 | "cloud.google.com/go/storage" 11 | hotmaze "github.com/Deleplace/hot-maze/B3" 12 | "golang.org/x/net/context" 13 | "golang.org/x/oauth2/google" 14 | ) 15 | 16 | const ( 17 | projectID = "hot-maze-2" 18 | backendBaseURL = "https://hotmaze.io" 19 | storageServiceAccountID = "ephemeral-storage@hot-maze-2.iam.gserviceaccount.com" 20 | bucket = "hot-maze-2" 21 | fileDeleteAfter = 9 * time.Minute 22 | taskQueue = "projects/hot-maze-2/locations/us-central1/queues/b3-file-expiry" 23 | secretPKPath = "projects/700343211022/secrets/B3-storage-private-key/versions/latest" 24 | ) 25 | 26 | func main() { 27 | ctx := context.Background() 28 | 29 | log.Println("GOOGLE_APPLICATION_CREDENTIALS =", os.Getenv("GOOGLE_APPLICATION_CREDENTIALS")) 30 | // log.Println(os.Environ()) 31 | cred, errCred := google.FindDefaultCredentials(ctx) 32 | if errCred != nil { 33 | log.Println("errCred =", errCred) 34 | } 35 | if cred != nil { 36 | log.Println("FindDefaultCredentials =", string(cred.JSON)) 37 | } 38 | 39 | storageClient, err := storage.NewClient(ctx) 40 | if err != nil { 41 | // log.Fatal("Couldn't create Storage client:", err) 42 | log.Println("Couldn't create Storage client:", err) 43 | } 44 | sa, errSA := storageClient.ServiceAccount(ctx, projectID) 45 | log.Println("storageClient.ServiceAccount is", sa, errSA) 46 | 47 | storagePrivateKey, errSecret := hotmaze.AccessSecretVersion(secretPKPath) 48 | if errSecret != nil { 49 | // log.Fatal("Couldn't read Storage service account private key:", err) 50 | log.Println("Couldn't read Storage service account private key:", errSecret) 51 | } 52 | 53 | server := hotmaze.Server{ 54 | GCPProjectID: projectID, 55 | BackendBaseURL: backendBaseURL, 56 | StorageClient: storageClient, 57 | StorageAccountID: storageServiceAccountID, 58 | StoragePrivateKey: storagePrivateKey, 59 | StorageBucket: bucket, 60 | StorageFileTTL: fileDeleteAfter, 61 | CloudTasksQueuePath: taskQueue, 62 | } 63 | server.RegisterHandlers() 64 | 65 | port := os.Getenv("PORT") 66 | if port == "" { 67 | port = "8080" 68 | log.Printf("Defaulting to port %s", port) 69 | } 70 | 71 | log.Printf("Listening on port %s", port) 72 | err = http.ListenAndServe(fmt.Sprintf(":%s", port), nil) 73 | log.Fatal(err) 74 | } 75 | -------------------------------------------------------------------------------- /template/web/static/css/hot-maze.css: -------------------------------------------------------------------------------- 1 | 2 | body { 3 | text-align: center; 4 | /*background-color: #64d8ff;*/ 5 | height: 100%; 6 | min-height: 500px; 7 | border: rgba(255, 255, 255, 0) solid 10px; 8 | } 9 | 10 | input[readonly] 11 | { 12 | background-color: white; 13 | } 14 | 15 | #progressZone { 16 | /* 17 | TODO: make sure you display the progress in the middle 18 | of the QR zone, only if the actual QR code is generated 19 | after the upload, not before the upload. 20 | */ 21 | margin-top: -160px; 22 | 23 | clear: both; 24 | min-height: 20px; 25 | padding-bottom: 25px; 26 | } 27 | 28 | #uploadProgress { 29 | /* Hidden at first. */ 30 | display: none; 31 | } 32 | 33 | #qrZone { 34 | clear: both; 35 | } 36 | 37 | #qrcode { 38 | text-align: center; 39 | } 40 | 41 | #qrcode img { 42 | margin: auto; 43 | border-radius: 24px; 44 | cursor: zoom-in; 45 | border: 10px solid white; 46 | } 47 | 48 | #resourceUpload { 49 | display: inline-block; 50 | background-image: url('../img/empty_QR.png'); 51 | background-repeat: no-repeat; 52 | min-height: calc(280px - 70px); 53 | min-width: calc(280px - 60px); 54 | padding-top: 70px; 55 | padding-left: 60px; 56 | border: white solid 10px; 57 | border-radius: 24px; 58 | } 59 | 60 | #resourceUpload.transfering { 61 | background-image: url('../img/empty_QR_transfering.png'); 62 | } 63 | 64 | #resourceUpload > form{ 65 | width: 180px; 66 | } 67 | 68 | #dropInvite { 69 | margin-bottom: 20px; 70 | font-size: 32px; 71 | font-weight: bold; 72 | color: #F77; 73 | } 74 | 75 | #help { 76 | position: fixed; 77 | right: 1em; 78 | margin-left: 10px; 79 | margin-bottom: 8px; 80 | background-color: white; 81 | border: 8px solid black; 82 | padding: 0 8px 0 8px; 83 | text-align: left; 84 | border-radius: 15px; 85 | min-width: 2em; 86 | } 87 | 88 | #questionMark { 89 | font-weight: bolder; 90 | font-size: 3em; 91 | cursor: pointer; 92 | padding: 4px 8px 4px 10px; 93 | margin: -2px 0 0 -2px; 94 | } 95 | 96 | #helpContents { 97 | width: 100%; 98 | } 99 | 100 | #helpContents img.right { 101 | float: right; 102 | margin: 12px; 103 | max-width: 50%; 104 | } 105 | 106 | #helpClose { 107 | float: right; 108 | margin-top: -0.6em; 109 | font-weight: bolder; 110 | font-size: 3em; 111 | cursor: pointer; 112 | } 113 | 114 | .link-to-dual { 115 | display: inline-block; 116 | width: 95%; 117 | margin-top: 20px; 118 | border-top: dotted #AAA; 119 | padding: 1em; 120 | } 121 | -------------------------------------------------------------------------------- /B2/frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Hot Maze - Send your document from computer to mobile 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 |
?
15 | 31 |
32 | 33 |
34 |
35 |
36 | 37 | 38 |
39 |
40 |
41 | 42 |
43 |
44 |
45 |
46 | 47 |
48 | 49 |
50 | 51 |
52 |
53 | 54 |
55 |
56 | 57 | 58 | 59 | 60 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /D1/download.go: -------------------------------------------------------------------------------- 1 | package hotmaze 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net/http" 7 | "strings" 8 | "time" 9 | 10 | "cloud.google.com/go/firestore" 11 | "google.golang.org/grpc/codes" 12 | "google.golang.org/grpc/status" 13 | ) 14 | 15 | // HandlerDownload reads file data from Firestore and writes it to w. 16 | // In worflow D1 it is possible to access this URL before the resource has 17 | // actually been uploaded. Thus in case of "not found", we retry every 400ms, 18 | // up to 50s. 19 | func (s Server) HandlerDownload(w http.ResponseWriter, r *http.Request) { 20 | fileUUID := strings.TrimPrefix(r.URL.Path, "/get/") 21 | log.Printf("Reading file %q\n", fileUUID) 22 | 23 | deadline := time.Now().Add(50 * time.Second) 24 | 25 | var err error 26 | var doc *firestore.DocumentSnapshot 27 | for time.Now().Before(deadline) { 28 | doc, err = s.FirestoreClient.Doc("D1/" + fileUUID + "_meta").Get(r.Context()) 29 | if status.Code(err) == codes.NotFound { 30 | log.Println("D1/" + fileUUID + "_meta not found yet...") 31 | time.Sleep(400 * time.Millisecond) 32 | continue 33 | } 34 | break 35 | } 36 | if err != nil { 37 | log.Println(err) 38 | http.Error(w, "Problem reading from Firestore :(", http.StatusInternalServerError) 39 | return 40 | } 41 | fields := doc.Data() 42 | fileSize64 := fields["size"].(int64) 43 | fileSize := int(fileSize64) 44 | 45 | fullData := make([]byte, fileSize) 46 | for k := 0; k*chunkSize < fileSize; k++ { 47 | log.Printf("Reading chunk %d\n", k) 48 | chunkPath := fmt.Sprintf("D1/%s_chunk_%d", fileUUID, k) 49 | for time.Now().Before(deadline) { 50 | doc, err = s.FirestoreClient.Doc(chunkPath).Get(r.Context()) 51 | if status.Code(err) == codes.NotFound { 52 | log.Println(chunkPath + " not found yet...") 53 | time.Sleep(400 * time.Millisecond) 54 | continue 55 | } 56 | break 57 | } 58 | if err != nil { 59 | log.Println(err) 60 | http.Error(w, "Problem reading chunk meta from Firestore :(", http.StatusInternalServerError) 61 | return 62 | } 63 | chunkFields := doc.Data() 64 | if chunkContents, ok := chunkFields["data"].([]byte); ok { 65 | copy(fullData[k*chunkSize:], chunkContents) 66 | } else { 67 | log.Println("chunk has no []byte field named 'data'") 68 | http.Error(w, "Problem reading chunk data from Firestore :(", http.StatusInternalServerError) 69 | return 70 | } 71 | } 72 | 73 | if ct, ok := fields["type"].(string); ok { 74 | w.Header().Set("Content-Type", ct) 75 | } 76 | _, err = w.Write(fullData) 77 | if err != nil { 78 | log.Println("Writing", len(fullData), "bytes of file contents response:", err) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /A00/web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Hot Maze - Send your document from computer to mobile 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 |
?
15 | 34 |
35 | 36 |
37 |
38 |
39 | 40 | 41 |
42 |
43 |
44 | 45 |
46 |
47 |
48 |
49 | 50 |
51 |
52 | 53 | 54 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /B1/fileexpiry.go: -------------------------------------------------------------------------------- 1 | package hotmaze 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "net/http" 8 | "time" 9 | 10 | cloudtasks "cloud.google.com/go/cloudtasks/apiv2" 11 | "cloud.google.com/go/storage" 12 | taskspb "google.golang.org/genproto/googleapis/cloud/tasks/v2" 13 | "google.golang.org/protobuf/types/known/timestamppb" 14 | ) 15 | 16 | func (s Server) HandlerForgetFile(w http.ResponseWriter, r *http.Request) { 17 | if r.Method != "POST" { 18 | http.Error(w, "POST only", http.StatusBadRequest) 19 | return 20 | } 21 | 22 | if taskName := r.Header.Get("X-Appengine-Taskname"); taskName == "" { 23 | // Validate the request comes from Cloud Tasks. 24 | log.Println("Invalid Task: No X-Appengine-Taskname request header found") 25 | http.Error(w, "Bad Request - Invalid Task", http.StatusBadRequest) 26 | return 27 | } 28 | 29 | fileUUID := r.FormValue("uuid") 30 | if fileUUID == "" { 31 | http.Error(w, "please provide file uuid", http.StatusBadRequest) 32 | return 33 | } 34 | 35 | objectName := "transit/" + fileUUID 36 | log.Println("Forgetting file", objectName) 37 | err := s.StorageClient.Bucket(s.StorageBucket).Object(objectName).Delete(r.Context()) 38 | switch { 39 | case err == storage.ErrObjectNotExist: 40 | log.Println("File", objectName, "did not exist") 41 | case err != nil: 42 | http.Error(w, err.Error(), http.StatusInternalServerError) 43 | return 44 | } 45 | } 46 | 47 | func (s Server) ScheduleForgetFile(ctx context.Context, fileUUID string) (*taskspb.Task, error) { 48 | // Adapted from https://cloud.google.com/tasks/docs/creating-appengine-tasks#go 49 | 50 | client, err := cloudtasks.NewClient(ctx) 51 | if err != nil { 52 | return nil, fmt.Errorf("cloudtasks.NewClient: %v", err) 53 | } 54 | defer client.Close() 55 | 56 | // Build the Task payload. 57 | // https://godoc.org/google.golang.org/genproto/googleapis/cloud/tasks/v2#CreateTaskRequest 58 | req := &taskspb.CreateTaskRequest{ 59 | Parent: s.CloudTasksQueuePath, 60 | Task: &taskspb.Task{ 61 | // https://godoc.org/google.golang.org/genproto/googleapis/cloud/tasks/v2#AppEngineHttpRequest 62 | MessageType: &taskspb.Task_AppEngineHttpRequest{ 63 | AppEngineHttpRequest: &taskspb.AppEngineHttpRequest{ 64 | HttpMethod: taskspb.HttpMethod_POST, 65 | RelativeUri: "/forget?uuid=" + fileUUID, 66 | AppEngineRouting: &taskspb.AppEngineRouting{ 67 | Version: "b1", 68 | }, 69 | }, 70 | }, 71 | ScheduleTime: ×tamppb.Timestamp{ 72 | Seconds: time.Now().Add(s.StorageFileTTL).Unix(), 73 | }, 74 | }, 75 | } 76 | 77 | createdTask, err := client.CreateTask(ctx, req) 78 | if err != nil { 79 | return nil, fmt.Errorf("CreateTask: %v", err) 80 | } 81 | 82 | return createdTask, nil 83 | } 84 | -------------------------------------------------------------------------------- /B3/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Hot Maze - Send your document from computer to mobile 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 |
?
15 | 31 |
32 | 33 |
34 |
35 |
36 | 37 | 38 |
39 |
40 |
41 | 42 |
43 |
44 |
45 |
46 | 47 |
48 | 49 |
50 | 51 |
52 |
53 | 54 |
55 |
56 | 57 | 58 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /B1/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Hot Maze - Send your document from computer to mobile 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 |
?
15 | 31 |
32 | 33 |
34 |
35 |
36 | 37 | 38 |
39 |
40 |
41 | 42 |
43 |
44 |
45 |
46 | 47 |
48 | 49 |
50 | 51 |
52 |
53 | 54 |
55 |
56 | 57 | 58 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /C1/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Hot Maze - Send your document from computer to mobile 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 |
?
15 | 31 |
32 | 33 |
34 |
35 |
36 | 37 | 38 |
39 |
40 |
41 | 42 |
43 |
44 |
45 |
46 | 47 |
48 | 49 |
50 | 51 |
52 |
53 | 54 |
55 |
56 | 57 | 58 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /D1/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Hot Maze - Send your document from computer to mobile 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 |
?
15 | 31 |
32 | 33 |
34 |
35 |
36 | 37 | 38 |
39 |
40 |
41 | 42 |
43 |
44 |
45 |
46 | 47 |
48 | 49 |
50 | 51 |
52 |
53 | 54 |
55 |
56 | 57 | 58 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /template/web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Hot Maze - Send your document from computer to mobile 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 |
?
15 | 34 |
35 | 36 | 44 | 45 |
46 |
47 |
48 |
49 | 50 |
51 | 52 |
53 | 54 |
55 |
56 | 57 | 58 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /D1/upload.go: -------------------------------------------------------------------------------- 1 | package hotmaze 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io/ioutil" 7 | "log" 8 | "net/http" 9 | "time" 10 | ) 11 | 12 | const ( 13 | validity = 300 * time.Second 14 | KB = 1024 15 | chunkSize = 512 * KB 16 | ) 17 | 18 | func (s Server) HandlerUpload(w http.ResponseWriter, r *http.Request) { 19 | if errCORS := s.accessControlAllowHotMaze(w, r); errCORS != nil { 20 | log.Println(errCORS) 21 | http.Error(w, errCORS.Error(), http.StatusBadRequest) 22 | return 23 | } 24 | 25 | if r.Method != "POST" { 26 | http.Error(w, "POST only", http.StatusBadRequest) 27 | return 28 | } 29 | 30 | fileData, err := ioutil.ReadAll(r.Body) 31 | if err != nil { 32 | http.Error(w, "Could not read file data :(", http.StatusInternalServerError) 33 | return 34 | } 35 | defer r.Body.Close() 36 | if len(fileData) == 0 { 37 | http.Error(w, "Empty file :(", http.StatusBadRequest) 38 | return 39 | } 40 | contentType := r.Header.Get("Content-Type") 41 | fileUUID := r.FormValue("uuid") 42 | if fileUUID == "" { 43 | http.Error(w, "Please provide uuid", http.StatusBadRequest) 44 | return 45 | } 46 | log.Println("Received resource", fileUUID, "of type", contentType, "and size", len(fileData)) 47 | 48 | _, errCreate := s.FirestoreClient.Doc("D1/"+fileUUID+"_meta").Create(r.Context(), map[string]interface{}{ 49 | "type": contentType, 50 | "size": len(fileData), 51 | "created": time.Now(), 52 | }) 53 | if errCreate != nil { 54 | log.Println("Writing resource meta:", errCreate) 55 | http.Error(w, "Problem writing to Firestore :(", http.StatusInternalServerError) 56 | return 57 | } 58 | 59 | // Write file contents in 512KB chunks. 60 | // (a Firestore document can never hold more than 1MB) 61 | nbChunks := (len(fileData) + chunkSize - 1) / chunkSize 62 | 63 | _, err = s.ScheduleForgetFile(r.Context(), fileUUID, nbChunks) 64 | if err != nil { 65 | log.Println("scheduling file expiry:", err) 66 | // Better fail now, than keeping a user file forever in Firestore 67 | http.Error(w, "Problem with file allocation :(", http.StatusInternalServerError) 68 | return 69 | } 70 | log.Println("File", fileUUID, "is scheduled for deletion") 71 | 72 | for k := 0; k*chunkSize < len(fileData); k++ { 73 | var chunk []byte 74 | if len(fileData) < (k+1)*chunkSize { 75 | chunk = fileData[k*chunkSize:] 76 | } else { 77 | chunk = fileData[k*chunkSize : (k+1)*chunkSize] 78 | } 79 | log.Println("For UUID", fileUUID, "writing chunk", k) 80 | chunkPath := fmt.Sprintf("D1/%s_chunk_%d", fileUUID, k) 81 | _, errCreate := s.FirestoreClient.Doc(chunkPath).Create(r.Context(), map[string]interface{}{ 82 | "data": chunk, 83 | }) 84 | if errCreate != nil { 85 | log.Println("Writing chunk:", errCreate) 86 | http.Error(w, "Problem writing to Firestore :(", http.StatusInternalServerError) 87 | return 88 | } 89 | } 90 | 91 | w.Header().Set("Content-Type", "application/json") 92 | json.NewEncoder(w).Encode(map[string]interface{}{ 93 | "downloadURL": s.BackendBaseURL + "/get/" + fileUUID, 94 | }) 95 | } 96 | -------------------------------------------------------------------------------- /B1/static/css/hot-maze.css: -------------------------------------------------------------------------------- 1 | 2 | body { 3 | text-align: center; 4 | /*background-color: #64d8ff;*/ 5 | height: 100%; 6 | min-height: 500px; 7 | border: rgba(255, 255, 255, 0) solid 10px; 8 | } 9 | 10 | input[readonly] 11 | { 12 | background-color: white; 13 | } 14 | 15 | #progressZone { 16 | clear: both; 17 | min-height: 20px; 18 | padding-bottom: 25px; 19 | } 20 | 21 | #uploadProgress { 22 | /* Hidden at first. */ 23 | display: none; 24 | /* 25 | TODO: make sure you display the progress in the middle 26 | of the QR zone, only if the actual QR code is generated 27 | after the upload, not before the upload. 28 | */ 29 | margin-top: -160px; 30 | margin-bottom: 160px; 31 | width: 10em; 32 | z-index: 100; 33 | } 34 | 35 | #qrZone { 36 | clear: both; 37 | } 38 | 39 | #qrcode { 40 | text-align: center; 41 | z-index: 80; 42 | } 43 | 44 | #qrcode img { 45 | margin: auto; 46 | border-radius: 24px; 47 | cursor: zoom-in; 48 | border: 10px solid white; 49 | } 50 | 51 | #resourceUpload { 52 | display: inline-block; 53 | background-image: url('../img/empty_QR.png'); 54 | background-repeat: no-repeat; 55 | min-height: calc(280px - 70px); 56 | min-width: calc(280px - 60px); 57 | padding-top: 70px; 58 | padding-left: 60px; 59 | border: white solid 10px; 60 | border-radius: 24px; 61 | } 62 | 63 | #resourceUpload.transfering { 64 | background-image: url('../img/empty_QR_transfering.png'); 65 | } 66 | 67 | #resourceUpload > form{ 68 | width: 180px; 69 | } 70 | 71 | #dropInvite { 72 | margin-bottom: 20px; 73 | font-size: 32px; 74 | font-weight: bold; 75 | color: #F77; 76 | } 77 | 78 | #help { 79 | position: fixed; 80 | z-index: 120; 81 | right: 1em; 82 | margin-left: 10px; 83 | margin-bottom: 8px; 84 | background-color: white; 85 | border: 8px solid black; 86 | padding: 0 8px 0 8px; 87 | text-align: left; 88 | border-radius: 15px; 89 | min-width: 2em; 90 | } 91 | 92 | #questionMark { 93 | font-weight: bolder; 94 | font-size: 3em; 95 | cursor: pointer; 96 | padding: 4px 8px 4px 10px; 97 | margin: -2px 0 0 -2px; 98 | } 99 | 100 | #helpContents { 101 | width: 100%; 102 | } 103 | 104 | #helpContents img.right { 105 | float: right; 106 | margin: 12px; 107 | max-width: 50%; 108 | } 109 | 110 | #helpContents p { 111 | padding-left: 1em; 112 | padding-right: 1em; 113 | } 114 | 115 | #helpClose { 116 | float: right; 117 | margin-top: -0.6em; 118 | font-weight: bolder; 119 | font-size: 3em; 120 | cursor: pointer; 121 | } 122 | 123 | .link-to-dual { 124 | display: inline-block; 125 | width: 95%; 126 | margin-top: 20px; 127 | border-top: dotted #AAA; 128 | padding: 1em; 129 | } 130 | 131 | #errorZone { 132 | display: none; 133 | background-color: #F77; 134 | border-radius:2em; 135 | padding: 1em; 136 | font-size: 2em; 137 | font-weight: bold; 138 | color: white; 139 | } 140 | 141 | #expirationZone { 142 | font-weight: bold; 143 | color: #666; 144 | } 145 | 146 | #expirationZone > p { 147 | margin: 0; 148 | } -------------------------------------------------------------------------------- /A00/web/static/css/hot-maze.css: -------------------------------------------------------------------------------- 1 | 2 | body { 3 | text-align: center; 4 | /*background-color: #64d8ff;*/ 5 | height: 100%; 6 | min-height: 500px; 7 | border: rgba(255, 255, 255, 0) solid 10px; 8 | font-family: sans-serif; 9 | } 10 | 11 | input[readonly] 12 | { 13 | background-color: white; 14 | } 15 | 16 | #progressZone { 17 | /* 18 | TODO: make sure you display the progress in the middle 19 | of the QR zoen, only if the actual QR code is generated 20 | after the upload, not before the upload. 21 | */ 22 | margin-top: -160px; 23 | 24 | clear: both; 25 | min-height: 20px; 26 | padding-bottom: 25px; 27 | } 28 | 29 | #uploadProgress { 30 | /* Hidden at first. */ 31 | display: none; 32 | } 33 | 34 | #qrZone { 35 | clear: both; 36 | } 37 | 38 | #qrcode { 39 | text-align: center; 40 | } 41 | 42 | #qrcode img { 43 | margin: auto; 44 | border-radius: 24px; 45 | cursor: zoom-in; 46 | border: 10px solid white; 47 | } 48 | 49 | #resourceUpload { 50 | display: inline-block; 51 | background-image: url('../img/empty_QR.png'); 52 | background-repeat: no-repeat; 53 | min-height: calc(280px - 70px); 54 | min-width: calc(280px - 60px); 55 | padding-top: 70px; 56 | padding-left: 60px; 57 | border: white solid 10px; 58 | border-radius: 24px; 59 | } 60 | 61 | #resourceUpload.transfering { 62 | background-image: url('../img/empty_QR_transfering.png'); 63 | } 64 | 65 | #resourceUpload > form{ 66 | width: 180px; 67 | } 68 | 69 | #dropInvite { 70 | margin-bottom: 20px; 71 | font-size: 32px; 72 | font-weight: bold; 73 | color: #F77; 74 | } 75 | 76 | #help { 77 | position: fixed; 78 | right: 1em; 79 | margin-left: 10px; 80 | margin-bottom: 8px; 81 | background-color: white; 82 | border: 8px solid black; 83 | padding: 0 8px 0 8px; 84 | text-align: left; 85 | border-radius: 15px; 86 | min-width: 2em; 87 | } 88 | 89 | #questionMark { 90 | font-weight: bolder; 91 | font-size: 3em; 92 | cursor: pointer; 93 | padding: 4px 8px 4px 10px; 94 | margin: -2px 0 0 -2px; 95 | } 96 | 97 | #helpContents { 98 | width: 100%; 99 | } 100 | 101 | #helpContents img.right { 102 | float: right; 103 | margin: 12px; 104 | max-width: 50%; 105 | } 106 | 107 | #helpClose { 108 | float: right; 109 | margin-top: -0.6em; 110 | font-weight: bolder; 111 | font-size: 3em; 112 | cursor: pointer; 113 | } 114 | 115 | ol.steps { 116 | display: inline-block; 117 | max-width: 45%; 118 | } 119 | 120 | .steps li { 121 | background-color: #EDB; 122 | padding: 0.5em; 123 | padding-right: 2em; 124 | margin-bottom: 1em; 125 | border-radius: 1em; 126 | border-bottom-right-radius: 3em; 127 | font-size: 2em; 128 | color: #543; 129 | font-weight: bolder; 130 | } 131 | 132 | .link-to-dual { 133 | display: inline-block; 134 | width: 95%; 135 | margin-top: 20px; 136 | border-top: dotted #AAA; 137 | padding: 1em; 138 | } 139 | 140 | #errorZone { 141 | display: none; 142 | background-color: #F77; 143 | border-radius:2em; 144 | padding: 1em; 145 | font-size: 2em; 146 | font-weight: bold; 147 | color: white; 148 | } -------------------------------------------------------------------------------- /D1/static/css/hot-maze.css: -------------------------------------------------------------------------------- 1 | 2 | body { 3 | text-align: center; 4 | /*background-color: #64d8ff;*/ 5 | height: 100%; 6 | min-height: 500px; 7 | border: rgba(255, 255, 255, 0) solid 10px; 8 | font-family: sans-serif; 9 | } 10 | 11 | input[readonly] 12 | { 13 | background-color: white; 14 | } 15 | 16 | #progressZone { 17 | clear: both; 18 | min-height: 20px; 19 | padding-bottom: 25px; 20 | } 21 | 22 | #uploadProgress { 23 | /* Hidden at first. */ 24 | display: none; 25 | width: 10em; 26 | z-index: 100; 27 | } 28 | 29 | #qrZone { 30 | clear: both; 31 | } 32 | 33 | #qrcode { 34 | text-align: center; 35 | z-index: 80; 36 | } 37 | 38 | #qrcode img { 39 | margin: auto; 40 | border-radius: 24px; 41 | cursor: zoom-in; 42 | border: 10px solid white; 43 | } 44 | 45 | #resourceUpload { 46 | display: inline-block; 47 | background-image: url('../img/empty_QR.png'); 48 | background-repeat: no-repeat; 49 | min-height: calc(280px - 70px); 50 | min-width: calc(280px - 60px); 51 | padding-top: 70px; 52 | padding-left: 60px; 53 | border: white solid 10px; 54 | border-radius: 24px; 55 | } 56 | 57 | #resourceUpload.transfering { 58 | background-image: url('../img/empty_QR_transfering.png'); 59 | } 60 | 61 | #resourceUpload > form{ 62 | width: 180px; 63 | } 64 | 65 | #dropInvite { 66 | margin-bottom: 20px; 67 | font-size: 32px; 68 | font-weight: bold; 69 | color: #F77; 70 | } 71 | 72 | #help { 73 | position: fixed; 74 | z-index: 120; 75 | right: 1em; 76 | margin-left: 10px; 77 | margin-bottom: 8px; 78 | background-color: white; 79 | border: 8px solid black; 80 | padding: 0 8px 0 8px; 81 | text-align: left; 82 | border-radius: 15px; 83 | min-width: 2em; 84 | } 85 | 86 | #questionMark { 87 | font-weight: bolder; 88 | font-size: 3em; 89 | cursor: pointer; 90 | padding: 4px 8px 4px 10px; 91 | margin: -2px 0 0 -2px; 92 | } 93 | 94 | #helpContents { 95 | width: 100%; 96 | } 97 | 98 | #helpContents img.right { 99 | float: right; 100 | margin: 12px; 101 | max-width: 50%; 102 | } 103 | 104 | #helpContents p { 105 | padding-left: 1em; 106 | padding-right: 1em; 107 | } 108 | 109 | #helpClose { 110 | float: right; 111 | margin-top: -0.6em; 112 | font-weight: bolder; 113 | font-size: 3em; 114 | cursor: pointer; 115 | } 116 | 117 | ol.steps { 118 | display: inline-block; 119 | max-width: 45%; 120 | } 121 | 122 | .steps li { 123 | background-color: #EDB; 124 | padding: 0.5em; 125 | padding-right: 2em; 126 | margin-bottom: 1em; 127 | border-radius: 1em; 128 | border-bottom-right-radius: 3em; 129 | font-size: 2em; 130 | color: #543; 131 | font-weight: bolder; 132 | } 133 | 134 | .link-to-dual { 135 | display: inline-block; 136 | width: 95%; 137 | margin-top: 20px; 138 | border-top: dotted #AAA; 139 | padding: 1em; 140 | } 141 | 142 | #errorZone { 143 | display: none; 144 | background-color: #F77; 145 | border-radius:2em; 146 | padding: 1em; 147 | font-size: 2em; 148 | font-weight: bold; 149 | color: white; 150 | } 151 | 152 | #expirationZone { 153 | font-weight: bold; 154 | color: #666; 155 | } 156 | 157 | #expirationZone > p { 158 | margin: 0; 159 | } -------------------------------------------------------------------------------- /D1/fileexpiry.go: -------------------------------------------------------------------------------- 1 | package hotmaze 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "net/http" 8 | "strconv" 9 | "time" 10 | 11 | taskspb "google.golang.org/genproto/googleapis/cloud/tasks/v2" 12 | "google.golang.org/protobuf/types/known/timestamppb" 13 | ) 14 | 15 | func (s Server) HandlerForgetFile(w http.ResponseWriter, r *http.Request) { 16 | if r.Method != "POST" { 17 | http.Error(w, "POST only", http.StatusBadRequest) 18 | return 19 | } 20 | 21 | if taskName := r.Header.Get("X-Appengine-Taskname"); taskName == "" { 22 | // Validate the request comes from Cloud Tasks. 23 | log.Println("Invalid Task: No X-Appengine-Taskname request header found") 24 | http.Error(w, "Bad Request - Invalid Task", http.StatusBadRequest) 25 | return 26 | } 27 | 28 | fileUUID := r.FormValue("uuid") 29 | if fileUUID == "" { 30 | http.Error(w, "please provide file uuid", http.StatusBadRequest) 31 | return 32 | } 33 | 34 | chunksStr := r.FormValue("chunks") 35 | if fileUUID == "" { 36 | http.Error(w, "please provide number of chunks", http.StatusBadRequest) 37 | return 38 | } 39 | chunks, err := strconv.Atoi(chunksStr) 40 | if err != nil { 41 | http.Error(w, "please provide proper number of chunks", http.StatusBadRequest) 42 | return 43 | } 44 | 45 | path := fmt.Sprintf("D1/%s_meta", fileUUID) 46 | log.Println("Deleting path", path, " from Firestore") 47 | _, errDelete := s.FirestoreClient.Doc(path).Delete(r.Context()) 48 | if errDelete != nil { 49 | log.Println(errDelete) 50 | http.Error(w, "Problem deleting a resource meta from Firestore :(", http.StatusInternalServerError) 51 | return 52 | } 53 | 54 | for k := 0; k < chunks; k++ { 55 | path := fmt.Sprintf("D1/%s_chunk_%d", fileUUID, k) 56 | log.Println("Deleting path", path, " from Firestore") 57 | _, errDelete := s.FirestoreClient.Doc(path).Delete(r.Context()) 58 | if errDelete != nil { 59 | log.Println(errDelete) 60 | http.Error(w, "Problem deleting a resource chunk from Firestore :(", http.StatusInternalServerError) 61 | return 62 | } 63 | } 64 | } 65 | 66 | func (s Server) ScheduleForgetFile(ctx context.Context, fileUUID string, nbChunks int) (*taskspb.Task, error) { 67 | uri := fmt.Sprintf("/forget?uuid=%s&chunks=%d", fileUUID, nbChunks) 68 | 69 | // Adapted from https://cloud.google.com/tasks/docs/creating-appengine-tasks#go 70 | 71 | // Build the Task payload. 72 | // https://godoc.org/google.golang.org/genproto/googleapis/cloud/tasks/v2#CreateTaskRequest 73 | req := &taskspb.CreateTaskRequest{ 74 | Parent: s.CloudTasksQueuePath, 75 | Task: &taskspb.Task{ 76 | // https://godoc.org/google.golang.org/genproto/googleapis/cloud/tasks/v2#AppEngineHttpRequest 77 | MessageType: &taskspb.Task_AppEngineHttpRequest{ 78 | AppEngineHttpRequest: &taskspb.AppEngineHttpRequest{ 79 | HttpMethod: taskspb.HttpMethod_POST, 80 | RelativeUri: uri, 81 | AppEngineRouting: &taskspb.AppEngineRouting{ 82 | Version: "d1", 83 | }, 84 | }, 85 | }, 86 | ScheduleTime: ×tamppb.Timestamp{ 87 | Seconds: time.Now().Add(s.StorageFileTTL).Unix(), 88 | }, 89 | }, 90 | } 91 | 92 | createdTask, err := s.TasksClient.CreateTask(ctx, req) 93 | if err != nil { 94 | return nil, fmt.Errorf("CreateTask: %v", err) 95 | } 96 | 97 | return createdTask, nil 98 | } 99 | -------------------------------------------------------------------------------- /C1/static/css/hot-maze.css: -------------------------------------------------------------------------------- 1 | 2 | body { 3 | text-align: center; 4 | /*background-color: #64d8ff;*/ 5 | height: 100%; 6 | min-height: 500px; 7 | border: rgba(255, 255, 255, 0) solid 10px; 8 | font-family: sans-serif; 9 | } 10 | 11 | input[readonly] 12 | { 13 | background-color: white; 14 | } 15 | 16 | #progressZone { 17 | clear: both; 18 | min-height: 20px; 19 | padding-bottom: 25px; 20 | } 21 | 22 | #uploadProgress { 23 | /* Hidden at first. */ 24 | display: none; 25 | /* 26 | TODO: make sure you display the progress in the middle 27 | of the QR zone, only if the actual QR code is generated 28 | after the upload, not before the upload. 29 | */ 30 | margin-top: -160px; 31 | margin-bottom: 160px; 32 | width: 10em; 33 | z-index: 100; 34 | } 35 | 36 | #qrZone { 37 | clear: both; 38 | } 39 | 40 | #qrcode { 41 | text-align: center; 42 | z-index: 80; 43 | } 44 | 45 | #qrcode img { 46 | margin: auto; 47 | border-radius: 24px; 48 | cursor: zoom-in; 49 | border: 10px solid white; 50 | } 51 | 52 | #resourceUpload { 53 | display: inline-block; 54 | background-image: url('../img/empty_QR.png'); 55 | background-repeat: no-repeat; 56 | min-height: calc(280px - 70px); 57 | min-width: calc(280px - 60px); 58 | padding-top: 70px; 59 | padding-left: 60px; 60 | border: white solid 10px; 61 | border-radius: 24px; 62 | } 63 | 64 | #resourceUpload.transfering { 65 | background-image: url('../img/empty_QR_transfering.png'); 66 | } 67 | 68 | #resourceUpload > form{ 69 | width: 180px; 70 | } 71 | 72 | #dropInvite { 73 | margin-bottom: 20px; 74 | font-size: 32px; 75 | font-weight: bold; 76 | color: #F77; 77 | } 78 | 79 | #help { 80 | position: fixed; 81 | z-index: 120; 82 | right: 1em; 83 | margin-left: 10px; 84 | margin-bottom: 8px; 85 | background-color: white; 86 | border: 8px solid black; 87 | padding: 0 8px 0 8px; 88 | text-align: left; 89 | border-radius: 15px; 90 | min-width: 2em; 91 | } 92 | 93 | #questionMark { 94 | font-weight: bolder; 95 | font-size: 3em; 96 | cursor: pointer; 97 | padding: 4px 8px 4px 10px; 98 | margin: -2px 0 0 -2px; 99 | } 100 | 101 | #helpContents { 102 | width: 100%; 103 | } 104 | 105 | #helpContents img.right { 106 | float: right; 107 | margin: 12px; 108 | max-width: 50%; 109 | } 110 | 111 | #helpContents p { 112 | padding-left: 1em; 113 | padding-right: 1em; 114 | } 115 | 116 | #helpClose { 117 | float: right; 118 | margin-top: -0.6em; 119 | font-weight: bolder; 120 | font-size: 3em; 121 | cursor: pointer; 122 | } 123 | 124 | ol.steps { 125 | display: inline-block; 126 | max-width: 45%; 127 | } 128 | 129 | .steps li { 130 | background-color: #EDB; 131 | padding: 0.5em; 132 | padding-right: 2em; 133 | margin-bottom: 1em; 134 | border-radius: 1em; 135 | border-bottom-right-radius: 3em; 136 | font-size: 2em; 137 | color: #543; 138 | font-weight: bolder; 139 | } 140 | 141 | .link-to-dual { 142 | display: inline-block; 143 | width: 95%; 144 | margin-top: 20px; 145 | border-top: dotted #AAA; 146 | padding: 1em; 147 | } 148 | 149 | #errorZone { 150 | display: none; 151 | background-color: #F77; 152 | border-radius:2em; 153 | padding: 1em; 154 | font-size: 2em; 155 | font-weight: bold; 156 | color: white; 157 | } 158 | 159 | #expirationZone { 160 | font-weight: bold; 161 | color: #666; 162 | } 163 | 164 | #expirationZone > p { 165 | margin: 0; 166 | } -------------------------------------------------------------------------------- /B2/frontend/public/css/hot-maze.css: -------------------------------------------------------------------------------- 1 | 2 | body { 3 | text-align: center; 4 | /*background-color: #64d8ff;*/ 5 | height: 100%; 6 | min-height: 500px; 7 | border: rgba(255, 255, 255, 0) solid 10px; 8 | font-family: sans-serif; 9 | } 10 | 11 | input[readonly] 12 | { 13 | background-color: white; 14 | } 15 | 16 | #progressZone { 17 | clear: both; 18 | min-height: 20px; 19 | padding-bottom: 25px; 20 | } 21 | 22 | #uploadProgress { 23 | /* Hidden at first. */ 24 | display: none; 25 | /* 26 | TODO: make sure you display the progress in the middle 27 | of the QR zone, only if the actual QR code is generated 28 | after the upload, not before the upload. 29 | */ 30 | margin-top: -160px; 31 | margin-bottom: 160px; 32 | width: 10em; 33 | z-index: 100; 34 | } 35 | 36 | #qrZone { 37 | clear: both; 38 | } 39 | 40 | #qrcode { 41 | text-align: center; 42 | z-index: 80; 43 | } 44 | 45 | #qrcode img { 46 | margin: auto; 47 | border-radius: 24px; 48 | cursor: zoom-in; 49 | border: 10px solid white; 50 | } 51 | 52 | #resourceUpload { 53 | display: inline-block; 54 | background-image: url('../img/empty_QR.png'); 55 | background-repeat: no-repeat; 56 | min-height: calc(280px - 70px); 57 | min-width: calc(280px - 60px); 58 | padding-top: 70px; 59 | padding-left: 60px; 60 | border: white solid 10px; 61 | border-radius: 24px; 62 | } 63 | 64 | #resourceUpload.transfering { 65 | background-image: url('../img/empty_QR_transfering.png'); 66 | } 67 | 68 | #resourceUpload > form{ 69 | width: 180px; 70 | } 71 | 72 | #dropInvite { 73 | margin-bottom: 20px; 74 | font-size: 32px; 75 | font-weight: bold; 76 | color: #F77; 77 | } 78 | 79 | #help { 80 | position: fixed; 81 | z-index: 120; 82 | right: 1em; 83 | margin-left: 10px; 84 | margin-bottom: 8px; 85 | background-color: white; 86 | border: 8px solid black; 87 | padding: 0 8px 0 8px; 88 | text-align: left; 89 | border-radius: 15px; 90 | min-width: 2em; 91 | } 92 | 93 | #questionMark { 94 | font-weight: bolder; 95 | font-size: 3em; 96 | cursor: pointer; 97 | padding: 4px 8px 4px 10px; 98 | margin: -2px 0 0 -2px; 99 | } 100 | 101 | #helpContents { 102 | width: 100%; 103 | } 104 | 105 | #helpContents img.right { 106 | float: right; 107 | margin: 12px; 108 | max-width: 50%; 109 | } 110 | 111 | #helpContents p { 112 | padding-left: 1em; 113 | padding-right: 1em; 114 | } 115 | 116 | #helpClose { 117 | float: right; 118 | margin-top: -0.6em; 119 | font-weight: bolder; 120 | font-size: 3em; 121 | cursor: pointer; 122 | } 123 | 124 | ol.steps { 125 | display: inline-block; 126 | max-width: 45%; 127 | } 128 | 129 | .steps li { 130 | background-color: #EDB; 131 | padding: 0.5em; 132 | padding-right: 2em; 133 | margin-bottom: 1em; 134 | border-radius: 1em; 135 | border-bottom-right-radius: 3em; 136 | font-size: 2em; 137 | color: #543; 138 | font-weight: bolder; 139 | } 140 | 141 | .link-to-dual { 142 | display: inline-block; 143 | width: 95%; 144 | margin-top: 20px; 145 | border-top: dotted #AAA; 146 | padding: 1em; 147 | } 148 | 149 | #errorZone { 150 | display: none; 151 | background-color: #F77; 152 | border-radius:2em; 153 | padding: 1em; 154 | font-size: 2em; 155 | font-weight: bold; 156 | color: white; 157 | } 158 | 159 | #expirationZone { 160 | font-weight: bold; 161 | color: #666; 162 | } 163 | 164 | #expirationZone > p { 165 | margin: 0; 166 | } -------------------------------------------------------------------------------- /B1/signedurls.go: -------------------------------------------------------------------------------- 1 | package hotmaze 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "log" 7 | "net/http" 8 | "strconv" 9 | "strings" 10 | "time" 11 | 12 | "cloud.google.com/go/storage" 13 | "github.com/google/uuid" 14 | ) 15 | 16 | const ( 17 | validity = 300 * time.Second 18 | ) 19 | 20 | func (s Server) HandlerGenerateSignedURLs(w http.ResponseWriter, r *http.Request) { 21 | if errCORS := s.accessControlAllowHotMaze(w, r); errCORS != nil { 22 | log.Println(errCORS) 23 | http.Error(w, errCORS.Error(), http.StatusBadRequest) 24 | return 25 | } 26 | 27 | if r.Method != "POST" { 28 | http.Error(w, "POST only", http.StatusBadRequest) 29 | return 30 | } 31 | 32 | ctx := context.Background() 33 | filesize, _ := strconv.Atoi(r.FormValue("filesize")) 34 | fileUUID, uploadURL, downloadURL, err := s.GenerateURLs( 35 | ctx, 36 | r.FormValue("filetype"), 37 | filesize, 38 | r.FormValue("filename"), 39 | ) 40 | if err != nil { 41 | log.Println("generating signed URLs:", err) 42 | http.Error(w, "Could not generate signed URLs :(", http.StatusInternalServerError) 43 | return 44 | } 45 | 46 | _, err = s.ScheduleForgetFile(ctx, fileUUID) 47 | if err != nil { 48 | log.Println("scheduling file expiry:", err) 49 | // Better fail now, than keeping a user file forever in GCS 50 | http.Error(w, "Problem with file allocation :(", http.StatusInternalServerError) 51 | return 52 | } 53 | 54 | json.NewEncoder(w).Encode(map[string]interface{}{ 55 | "uploadURL": uploadURL, 56 | "downloadURL": downloadURL, 57 | }) 58 | } 59 | 60 | func (s Server) GenerateURLs( 61 | ctx context.Context, 62 | fileType string, 63 | fileSize int, 64 | filename string, 65 | 66 | ) (fileUUID, uploadURL, downloadURL string, err error) { 67 | fileUUID = uuid.New().String() 68 | objectName := "transit/" + fileUUID 69 | log.Printf("Creating URLs for ephemeral resource %q\n", objectName) 70 | 71 | uploadURL, err = storage.SignedURL( 72 | s.StorageBucket, 73 | objectName, 74 | &storage.SignedURLOptions{ 75 | GoogleAccessID: s.StorageAccountID, 76 | PrivateKey: s.StoragePrivateKey, 77 | Method: "PUT", 78 | Expires: time.Now().Add(validity), 79 | ContentType: fileType, 80 | }) 81 | if err != nil { 82 | return 83 | } 84 | 85 | // Instead of the full download signed URL which is too big to fit 86 | // comfortably in a QR-code, we return a short voucher instead. 87 | downloadURL = s.BackendBaseURL + "/get/" + fileUUID 88 | 89 | return 90 | } 91 | 92 | // HandlerUnshortenGetURL redirects a "short" URL to a "long" signed URL. 93 | // Short URL has length ~80. 94 | // Signed download URL has length ~550. 95 | func (s Server) HandlerUnshortenGetURL(w http.ResponseWriter, r *http.Request) { 96 | fileUUID := strings.TrimPrefix(r.URL.Path, "/get/") 97 | objectName := "transit/" + fileUUID 98 | log.Printf("Redirecting to a new download signed URL for ephemeral resource %q\n", objectName) 99 | 100 | downloadURL, err := storage.SignedURL( 101 | s.StorageBucket, 102 | objectName, 103 | &storage.SignedURLOptions{ 104 | GoogleAccessID: s.StorageAccountID, 105 | PrivateKey: s.StoragePrivateKey, 106 | Method: "GET", 107 | Expires: time.Now().Add(validity), 108 | }) 109 | if err != nil { 110 | log.Println("generating download signed URL:", err) 111 | http.Error(w, "Could not generate download signed URL :(", http.StatusInternalServerError) 112 | return 113 | } 114 | 115 | http.Redirect(w, r, downloadURL, http.StatusFound) 116 | } 117 | -------------------------------------------------------------------------------- /B2/backend/signedurls.go: -------------------------------------------------------------------------------- 1 | package hotmaze 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "log" 7 | "net/http" 8 | "strconv" 9 | "strings" 10 | "time" 11 | 12 | "cloud.google.com/go/storage" 13 | "github.com/google/uuid" 14 | ) 15 | 16 | const ( 17 | validity = 300 * time.Second 18 | ) 19 | 20 | func (s Server) HandlerGenerateSignedURLs(w http.ResponseWriter, r *http.Request) { 21 | if errCORS := s.accessControlAllowHotMaze(w, r); errCORS != nil { 22 | log.Println(errCORS) 23 | http.Error(w, errCORS.Error(), http.StatusBadRequest) 24 | return 25 | } 26 | 27 | if r.Method != "POST" { 28 | http.Error(w, "POST only", http.StatusBadRequest) 29 | return 30 | } 31 | 32 | ctx := context.Background() 33 | filesize, _ := strconv.Atoi(r.FormValue("filesize")) 34 | fileUUID, uploadURL, downloadURL, err := s.GenerateURLs( 35 | ctx, 36 | r.FormValue("filetype"), 37 | filesize, 38 | r.FormValue("filename"), 39 | ) 40 | if err != nil { 41 | log.Println("generating signed URLs:", err) 42 | http.Error(w, "Could not generate signed URLs :(", http.StatusInternalServerError) 43 | return 44 | } 45 | 46 | _, err = s.ScheduleForgetFile(ctx, fileUUID) 47 | if err != nil { 48 | log.Println("scheduling file expiry:", err) 49 | // Better fail now, than keeping a user file forever in GCS 50 | http.Error(w, "Problem with file allocation :(", http.StatusInternalServerError) 51 | return 52 | } 53 | 54 | json.NewEncoder(w).Encode(map[string]interface{}{ 55 | "uploadURL": uploadURL, 56 | "downloadURL": downloadURL, 57 | }) 58 | } 59 | 60 | func (s Server) GenerateURLs( 61 | ctx context.Context, 62 | fileType string, 63 | fileSize int, 64 | filename string, 65 | 66 | ) (fileUUID, uploadURL, downloadURL string, err error) { 67 | fileUUID = uuid.New().String() 68 | objectName := "transit/" + fileUUID 69 | log.Printf("Creating URLs for ephemeral resource %q\n", objectName) 70 | 71 | uploadURL, err = storage.SignedURL( 72 | s.StorageBucket, 73 | objectName, 74 | &storage.SignedURLOptions{ 75 | GoogleAccessID: s.StorageAccountID, 76 | PrivateKey: s.StoragePrivateKey, 77 | Method: "PUT", 78 | Expires: time.Now().Add(validity), 79 | ContentType: fileType, 80 | }) 81 | if err != nil { 82 | return 83 | } 84 | 85 | // Instead of the full download signed URL which is too big to fit 86 | // comfortably in a QR-code, we return a short voucher instead. 87 | downloadURL = s.BackendBaseURL + "/B2_Get/" + fileUUID 88 | 89 | return 90 | } 91 | 92 | // HandlerUnshortenGetURL redirects a "short" URL to a "long" signed URL. 93 | // Short URL has length ~80. 94 | // Signed download URL has length ~550. 95 | func (s Server) HandlerUnshortenGetURL(w http.ResponseWriter, r *http.Request) { 96 | parts := strings.Split(r.URL.Path, "/") 97 | fileUUID := parts[len(parts)-1] 98 | objectName := "transit/" + fileUUID 99 | log.Printf("Redirecting to a new download signed URL for ephemeral resource %q\n", objectName) 100 | 101 | downloadURL, err := storage.SignedURL( 102 | s.StorageBucket, 103 | objectName, 104 | &storage.SignedURLOptions{ 105 | GoogleAccessID: s.StorageAccountID, 106 | PrivateKey: s.StoragePrivateKey, 107 | Method: "GET", 108 | Expires: time.Now().Add(validity), 109 | }) 110 | if err != nil { 111 | log.Println("generating download signed URL:", err) 112 | http.Error(w, "Could not generate download signed URL :(", http.StatusInternalServerError) 113 | return 114 | } 115 | 116 | http.Redirect(w, r, downloadURL, http.StatusFound) 117 | } 118 | -------------------------------------------------------------------------------- /B3/signedurls.go: -------------------------------------------------------------------------------- 1 | package hotmaze 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "log" 7 | "net/http" 8 | "strconv" 9 | "strings" 10 | "time" 11 | 12 | "cloud.google.com/go/storage" 13 | "github.com/google/uuid" 14 | ) 15 | 16 | const ( 17 | validity = 300 * time.Second 18 | ) 19 | 20 | func (s *Server) HandlerGenerateSignedURLs(w http.ResponseWriter, r *http.Request) { 21 | if errCORS := s.accessControlAllowHotMaze(w, r); errCORS != nil { 22 | log.Println(errCORS) 23 | http.Error(w, errCORS.Error(), http.StatusBadRequest) 24 | return 25 | } 26 | 27 | if r.Method != "POST" { 28 | http.Error(w, "POST only", http.StatusBadRequest) 29 | return 30 | } 31 | 32 | ctx := context.Background() 33 | filesize, _ := strconv.Atoi(r.FormValue("filesize")) 34 | fileUUID, uploadURL, downloadURL, err := s.GenerateURLs( 35 | ctx, 36 | r.FormValue("filetype"), 37 | filesize, 38 | r.FormValue("filename"), 39 | ) 40 | if err != nil { 41 | log.Println("generating signed URLs:", err) 42 | http.Error(w, "Could not generate signed URLs :(", http.StatusInternalServerError) 43 | return 44 | } 45 | 46 | _, err = s.ScheduleForgetFile(ctx, fileUUID) 47 | if err != nil { 48 | log.Println("scheduling file expiry:", err) 49 | // Better fail now, than keeping a user file forever in GCS 50 | http.Error(w, "Problem with file allocation :(", http.StatusInternalServerError) 51 | return 52 | } 53 | log.Printf("Scheduled deletion for file UUID %q", fileUUID) 54 | 55 | json.NewEncoder(w).Encode(map[string]interface{}{ 56 | "uploadURL": uploadURL, 57 | "downloadURL": downloadURL, 58 | }) 59 | } 60 | 61 | func (s *Server) GenerateURLs( 62 | ctx context.Context, 63 | fileType string, 64 | fileSize int, 65 | filename string, 66 | 67 | ) (fileUUID, uploadURL, downloadURL string, err error) { 68 | fileUUID = uuid.New().String() 69 | objectName := "transit/" + fileUUID 70 | log.Printf("Creating URLs for ephemeral resource %q\n", objectName) 71 | 72 | uploadURL, err = storage.SignedURL( 73 | s.StorageBucket, 74 | objectName, 75 | &storage.SignedURLOptions{ 76 | GoogleAccessID: s.StorageAccountID, 77 | PrivateKey: s.StoragePrivateKey, 78 | Method: "PUT", 79 | Expires: time.Now().Add(validity), 80 | ContentType: fileType, 81 | }) 82 | if err != nil { 83 | return 84 | } 85 | 86 | // Instead of the full download signed URL which is too big to fit 87 | // comfortably in a QR-code, we return a short voucher instead. 88 | downloadURL = s.BackendBaseURL + "/get/" + fileUUID 89 | 90 | return 91 | } 92 | 93 | // HandlerUnshortenGetURL redirects a "short" URL to a "long" signed URL. 94 | // Short URL has length ~80. 95 | // Signed download URL has length ~550. 96 | func (s *Server) HandlerUnshortenGetURL(w http.ResponseWriter, r *http.Request) { 97 | fileUUID := strings.TrimPrefix(r.URL.Path, "/get/") 98 | objectName := "transit/" + fileUUID 99 | log.Printf("Redirecting to a new download signed URL for ephemeral resource %q\n", objectName) 100 | 101 | downloadURL, err := storage.SignedURL( 102 | s.StorageBucket, 103 | objectName, 104 | &storage.SignedURLOptions{ 105 | GoogleAccessID: s.StorageAccountID, 106 | PrivateKey: s.StoragePrivateKey, 107 | Method: "GET", 108 | Expires: time.Now().Add(validity), 109 | }) 110 | if err != nil { 111 | log.Println("generating download signed URL:", err) 112 | http.Error(w, "Could not generate download signed URL :(", http.StatusInternalServerError) 113 | return 114 | } 115 | 116 | http.Redirect(w, r, downloadURL, http.StatusFound) 117 | } 118 | -------------------------------------------------------------------------------- /B3/static/css/hot-maze.css: -------------------------------------------------------------------------------- 1 | 2 | body { 3 | text-align: center; 4 | /*background-color: #64d8ff;*/ 5 | height: 100%; 6 | min-height: 500px; 7 | border: rgba(255, 255, 255, 0) solid 10px; 8 | font-family: sans-serif; 9 | } 10 | 11 | input[readonly] 12 | { 13 | background-color: white; 14 | } 15 | 16 | #progressZone { 17 | clear: both; 18 | min-height: 20px; 19 | padding-bottom: 25px; 20 | } 21 | 22 | #uploadProgress { 23 | /* Hidden at first. */ 24 | display: none; 25 | /* 26 | TODO: make sure you display the progress in the middle 27 | of the QR zone, only if the actual QR code is generated 28 | after the upload, not before the upload. 29 | */ 30 | margin-top: -160px; 31 | margin-bottom: 160px; 32 | width: 10em; 33 | z-index: 100; 34 | } 35 | 36 | #qrZone { 37 | clear: both; 38 | } 39 | 40 | #qrcode { 41 | text-align: center; 42 | z-index: 80; 43 | } 44 | 45 | #qrcode img { 46 | margin: auto; 47 | border-radius: 24px; 48 | cursor: zoom-in; 49 | border: 10px solid white; 50 | } 51 | 52 | #resourceUpload { 53 | display: inline-block; 54 | background-image: url('../img/empty_QR.png'); 55 | background-repeat: no-repeat; 56 | min-height: calc(280px - 70px); 57 | min-width: calc(280px - 60px); 58 | padding-top: 70px; 59 | padding-left: 60px; 60 | border: white solid 10px; 61 | border-radius: 24px; 62 | } 63 | 64 | #resourceUpload.transfering { 65 | background-image: url('../img/empty_QR_transfering.png'); 66 | } 67 | 68 | #resourceUpload > form{ 69 | width: 180px; 70 | } 71 | 72 | #dropInvite { 73 | margin-bottom: 20px; 74 | font-size: 32px; 75 | font-weight: bold; 76 | color: #F77; 77 | } 78 | 79 | #help { 80 | position: fixed; 81 | z-index: 120; 82 | right: 1em; 83 | margin-left: 10px; 84 | margin-bottom: 8px; 85 | background-color: white; 86 | border: 8px solid black; 87 | padding: 0 8px 0 8px; 88 | text-align: left; 89 | border-radius: 15px; 90 | min-width: 2em; 91 | } 92 | 93 | #questionMark { 94 | font-weight: bolder; 95 | font-size: 3em; 96 | cursor: pointer; 97 | padding: 4px 8px 4px 10px; 98 | margin: -2px 0 0 -2px; 99 | } 100 | 101 | #helpContents { 102 | width: 100%; 103 | } 104 | 105 | #helpContents img.right { 106 | float: right; 107 | margin: 12px; 108 | max-width: 50%; 109 | } 110 | 111 | #helpContents p { 112 | padding-left: 1em; 113 | padding-right: 1em; 114 | } 115 | 116 | #helpClose { 117 | float: right; 118 | margin-top: -0.4em; 119 | font-weight: bolder; 120 | font-size: 2.5em; 121 | cursor: pointer; 122 | } 123 | 124 | ol.steps { 125 | display: inline-block; 126 | max-width: 45%; 127 | } 128 | 129 | .steps li { 130 | background-color: #EDB; 131 | padding: 0.5em; 132 | padding-right: 2em; 133 | margin-bottom: 1em; 134 | border-radius: 1em; 135 | border-bottom-right-radius: 3em; 136 | font-size: 2em; 137 | color: #543; 138 | font-weight: bolder; 139 | } 140 | 141 | .link-to-dual { 142 | display: inline-block; 143 | width: 95%; 144 | margin-top: 20px; 145 | border-top: dotted #AAA; 146 | padding: 1em; 147 | } 148 | 149 | #errorZone { 150 | display: none; 151 | background-color: #F77; 152 | border-radius:2em; 153 | padding: 1em; 154 | font-size: 2em; 155 | font-weight: bold; 156 | color: white; 157 | } 158 | 159 | #expirationZone { 160 | font-weight: bold; 161 | color: #666; 162 | } 163 | 164 | #expirationZone > p { 165 | margin: 0; 166 | } 167 | 168 | @media (prefers-color-scheme: dark) { 169 | body { 170 | background-color: #012; 171 | color: #DDD; 172 | } 173 | #help, 174 | #resourceUpload { 175 | color: #111; 176 | } 177 | #expirationZone { 178 | color: #999; 179 | } 180 | } -------------------------------------------------------------------------------- /B2/frontend/public/terms.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Hot Maze - Privacy terms 6 | 7 | 12 | 13 | 14 | 15 |

Hot Maze privacy terms

16 | 17 |

18 | These terms apply since September, 2020. 19 |

20 | 21 |

22 | These terms may evolve. In case this happens, the new terms will apply to data transfered after the new terms have been published. The new terms won't revoke protections granted to previously transfered data. 23 |

24 | 25 |

26 | Hot Maze (the Service) is an online system to transfer data from a source device A to a target device B. 27 |

28 | 29 |

30 | The User must be in control of the browser of the source device A, of the target device B, and of the data D to be transfered from A to B. 31 |

32 | 33 |

34 | The Service is provided free of charge for the User. 35 |

36 | 37 |

38 | The Service is provided without any guarantee, except the best-effort policy to respect the privacy of the personal data of its Users. 39 |

40 | 41 |

42 | The Service doesn't use and doesn't store the identity of its Users. 43 |

44 | 45 |

46 | The only personal data involved is the payload D that the User explicitly requests to transit by the Service, using an upload action. 47 |

48 | 49 |

50 | The data D sent from A to B is routed through intermediary servers: the Brokers and the File server. 51 |

52 | 53 |

54 | All data transfers are protected by HTTPS protocol: 55 |

    56 |
  • Web pages from Service to User web browser
  • 57 |
  • Files from User device A to File server
  • 58 |
  • Files from File server to target mobile B
  • 59 |
60 |

61 | 62 |

63 | The transfered data D is kept private and is never made publicly available. 64 |

65 | 66 |

67 | The transfered data D is not disclosed to third parties. 68 |

69 | 70 |

71 | The transfered files are provided to the target B through short-lived URLs. The URLs expire after a maximum of ten minutes. This means that after ten minutes, the User cannot use a specific URL anymore. Expiring URLs do not prevent the User from saving the transfered files to the storage of target device B. 72 |

73 | 74 |

75 | Hot Maze currently doesn't implement end-to-end encryption. This means that the service owner could (theorically) read the data D transiting through the Service. 76 |

77 | 78 |

79 | The Service uses cloud computing infrastructure providers. The facilities are Firebase, Google Cloud Functions, and Google Cloud Storage. The infrastructure providers are bound by explicit privacy policies. However, it is technically feasible that they read the data D transiting through the Service. Whether this would constitute a violation of their own policies is debatable on a case-by-case basis. 80 |

81 | 82 |

83 | Anonymous data may be gathered for statistics purpose. This data includes, and is not limited to: 84 |

    85 |
  • country and city of usage,
  • 86 |
  • size and type of transfered data D,
  • 87 |
  • actions made by the same User,
  • 88 |
  • software quality metrics, such as the timing performances of Service processings.
  • 89 |
90 |

91 | 92 |

Contact

93 | 94 |

95 | deleplace2015@gmail.com 96 |

97 | 98 | 99 | 106 | 107 | -------------------------------------------------------------------------------- /B1/static/terms.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Hot Maze - Privacy terms 6 | 7 | 12 | 13 | 14 | 15 |

Hot Maze privacy terms

16 | 17 |

18 | These terms apply since September, 2020. 19 |

20 | 21 |

22 | These terms may evolve. In case this happens, the new terms will apply to data transfered after the new terms have been published. The new terms won't revoke protections granted to previously transfered data. 23 |

24 | 25 |

26 | Hot Maze (the Service) is an online system to transfer data from a source device A to a target device B. 27 |

28 | 29 |

30 | The User must be in control of the browser of the source device A, of the target device B, and of the data D to be transfered from A to B. 31 |

32 | 33 |

34 | The Service is provided free of charge for the User. 35 |

36 | 37 |

38 | The Service is provided without any guarantee, except the best-effort policy to respect the privacy of the personal data of its Users. 39 |

40 | 41 |

42 | The Service doesn't use and doesn't store the identity of its Users. 43 |

44 | 45 |

46 | The only personal data involved is the payload D that the User explicitly requests to transit by the Service, using an upload action. 47 |

48 | 49 |

50 | The data D sent from A to B is routed through intermediary servers: the Brokers and the File server. 51 |

52 | 53 |

54 | All data transfers are protected by HTTPS protocol: 55 |

    56 |
  • Web pages from Service to User web browser
  • 57 |
  • Files from User device A to File server
  • 58 |
  • Files from File server to target mobile B
  • 59 |
60 |

61 | 62 |

63 | The transfered data D is kept private and is never made publicly available. 64 |

65 | 66 |

67 | The transfered data D is not disclosed to third parties. 68 |

69 | 70 |

71 | The transfered files are provided to the target B through short-lived URLs. The URLs expire after a maximum of ten minutes. This means that after ten minutes, the User cannot use a specific URL anymore. Expiring URLs do not prevent the User from saving the transfered files to the storage of target device B. 72 |

73 | 74 |

75 | Hot Maze currently doesn't implement end-to-end encryption. This means that the service owner could (theorically) read the data D transiting through the Service. 76 |

77 | 78 |

79 | The Service uses cloud computing infrastructure providers. The facilities are Google App Engine and Google Cloud Storage. The infrastructure providers are bound by explicit privacy policies. However, it is technically feasible that they read the data D transiting through the Service. Whether this would constitute a violation of their own policies is debatable on a case-by-case basis. 80 |

81 | 82 |

83 | Anonymous data may be gathered for statistics purpose. This data includes, and is not limited to: 84 |

    85 |
  • country and city of usage,
  • 86 |
  • size and type of transfered data D,
  • 87 |
  • actions made by the same User,
  • 88 |
  • software quality metrics, such as the timing performances of Service processings.
  • 89 |
90 |

91 | 92 |

Contact

93 | 94 |

95 | deleplace2015@gmail.com 96 |

97 | 98 | 107 | 108 | -------------------------------------------------------------------------------- /B3/static/terms.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Hot Maze - Privacy terms 6 | 7 | 12 | 13 | 14 | 15 |

Hot Maze privacy terms

16 | 17 |

18 | These terms apply since September, 2020. 19 |

20 | 21 |

22 | These terms may evolve. In case this happens, the new terms will apply to data transfered after the new terms have been published. The new terms won't revoke protections granted to previously transfered data. 23 |

24 | 25 |

26 | Hot Maze (the Service) is an online system to transfer data from a source device A to a target device B. 27 |

28 | 29 |

30 | The User must be in control of the browser of the source device A, of the target device B, and of the data D to be transfered from A to B. 31 |

32 | 33 |

34 | The Service is provided free of charge for the User. 35 |

36 | 37 |

38 | The Service is provided without any guarantee, except the best-effort policy to respect the privacy of the personal data of its Users. 39 |

40 | 41 |

42 | The Service doesn't use and doesn't store the identity of its Users. 43 |

44 | 45 |

46 | The only personal data involved is the payload D that the User explicitly requests to transit by the Service, using an upload action. 47 |

48 | 49 |

50 | The data D sent from A to B is routed through intermediary servers: the Brokers and the File server. 51 |

52 | 53 |

54 | All data transfers are protected by HTTPS protocol: 55 |

    56 |
  • Web pages from Service to User web browser
  • 57 |
  • Files from User device A to File server
  • 58 |
  • Files from File server to target mobile B
  • 59 |
60 |

61 | 62 |

63 | The transfered data D is kept private and is never made publicly available. 64 |

65 | 66 |

67 | The transfered data D is not disclosed to third parties. 68 |

69 | 70 |

71 | The transfered files are provided to the target B through short-lived URLs. The URLs expire after a maximum of ten minutes. This means that after ten minutes, the User cannot use a specific URL anymore. Expiring URLs do not prevent the User from saving the transfered files to the storage of target device B. 72 |

73 | 74 |

75 | Hot Maze currently doesn't implement end-to-end encryption. This means that the service owner could (theorically) read the data D transiting through the Service. 76 |

77 | 78 |

79 | The Service uses cloud computing infrastructure providers. The facilities are Google Cloud Run and Google Cloud Storage. The infrastructure providers are bound by explicit privacy policies. However, it is technically feasible that they read the data D transiting through the Service. Whether this would constitute a violation of their own policies is debatable on a case-by-case basis. 80 |

81 | 82 |

83 | Anonymous data may be gathered for statistics purpose. This data includes, and is not limited to: 84 |

    85 |
  • country and city of usage,
  • 86 |
  • size and type of transfered data D,
  • 87 |
  • actions made by the same User,
  • 88 |
  • software quality metrics, such as the timing performances of Service processings.
  • 89 |
90 |

91 | 92 |

Contact

93 | 94 |

95 | deleplace2015@gmail.com 96 |

97 | 98 | 107 | 108 | -------------------------------------------------------------------------------- /C1/static/terms.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Hot Maze - Privacy terms 6 | 7 | 12 | 13 | 14 | 15 |

Hot Maze privacy terms

16 | 17 |

18 | These terms apply since September, 2020. 19 |

20 | 21 |

22 | These terms may evolve. In case this happens, the new terms will apply to data transfered after the new terms have been published. The new terms won't revoke protections granted to previously transfered data. 23 |

24 | 25 |

26 | Hot Maze (the Service) is an online system to transfer data from a source device A to a target device B. 27 |

28 | 29 |

30 | The User must be in control of the browser of the source device A, of the target device B, and of the data D to be transfered from A to B. 31 |

32 | 33 |

34 | The Service is provided free of charge for the User. 35 |

36 | 37 |

38 | The Service is provided without any guarantee, except the best-effort policy to respect the privacy of the personal data of its Users. 39 |

40 | 41 |

42 | The Service doesn't use and doesn't store the identity of its Users. 43 |

44 | 45 |

46 | The only personal data involved is the payload D that the User explicitly requests to transit by the Service, using an upload action. 47 |

48 | 49 |

50 | The data D sent from A to B is routed through intermediary servers: the Brokers and the File server. 51 |

52 | 53 |

54 | All data transfers are protected by HTTPS protocol: 55 |

    56 |
  • Web pages from Service to User web browser
  • 57 |
  • Files from User device A to File server
  • 58 |
  • Files from File server to target mobile B
  • 59 |
60 |

61 | 62 |

63 | The transfered data D is kept private and is never made publicly available. 64 |

65 | 66 |

67 | The transfered data D is not disclosed to third parties. 68 |

69 | 70 |

71 | The transfered files are provided to the target B through short-lived URLs. The URLs expire after a maximum of ten minutes. This means that after ten minutes, the User cannot use a specific URL anymore. Expiring URLs do not prevent the User from saving the transfered files to the storage of target device B. 72 |

73 | 74 |

75 | Hot Maze currently doesn't implement end-to-end encryption. This means that the service owner could (theorically) read the data D transiting through the Service. 76 |

77 | 78 |

79 | The Service uses cloud computing infrastructure providers. The facilities are Google App Engine and Google Cloud Firestore. The infrastructure providers are bound by explicit privacy policies. However, it is technically feasible that they read the data D transiting through the Service. Whether this would constitute a violation of their own policies is debatable on a case-by-case basis. 80 |

81 | 82 |

83 | Anonymous data may be gathered for statistics purpose. This data includes, and is not limited to: 84 |

    85 |
  • country and city of usage,
  • 86 |
  • size and type of transfered data D,
  • 87 |
  • actions made by the same User,
  • 88 |
  • software quality metrics, such as the timing performances of Service processings.
  • 89 |
90 |

91 | 92 |

Contact

93 | 94 |

95 | deleplace2015@gmail.com 96 |

97 | 98 | 107 | 108 | -------------------------------------------------------------------------------- /D1/static/terms.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Hot Maze - Privacy terms 6 | 7 | 12 | 13 | 14 | 15 |

Hot Maze privacy terms

16 | 17 |

18 | These terms apply since September, 2020. 19 |

20 | 21 |

22 | These terms may evolve. In case this happens, the new terms will apply to data transfered after the new terms have been published. The new terms won't revoke protections granted to previously transfered data. 23 |

24 | 25 |

26 | Hot Maze (the Service) is an online system to transfer data from a source device A to a target device B. 27 |

28 | 29 |

30 | The User must be in control of the browser of the source device A, of the target device B, and of the data D to be transfered from A to B. 31 |

32 | 33 |

34 | The Service is provided free of charge for the User. 35 |

36 | 37 |

38 | The Service is provided without any guarantee, except the best-effort policy to respect the privacy of the personal data of its Users. 39 |

40 | 41 |

42 | The Service doesn't use and doesn't store the identity of its Users. 43 |

44 | 45 |

46 | The only personal data involved is the payload D that the User explicitly requests to transit by the Service, using an upload action. 47 |

48 | 49 |

50 | The data D sent from A to B is routed through intermediary servers: the Brokers and the File server. 51 |

52 | 53 |

54 | All data transfers are protected by HTTPS protocol: 55 |

    56 |
  • Web pages from Service to User web browser
  • 57 |
  • Files from User device A to File server
  • 58 |
  • Files from File server to target mobile B
  • 59 |
60 |

61 | 62 |

63 | The transfered data D is kept private and is never made publicly available. 64 |

65 | 66 |

67 | The transfered data D is not disclosed to third parties. 68 |

69 | 70 |

71 | The transfered files are provided to the target B through short-lived URLs. The URLs expire after a maximum of ten minutes. This means that after ten minutes, the User cannot use a specific URL anymore. Expiring URLs do not prevent the User from saving the transfered files to the storage of target device B. 72 |

73 | 74 |

75 | Hot Maze currently doesn't implement end-to-end encryption. This means that the service owner could (theorically) read the data D transiting through the Service. 76 |

77 | 78 |

79 | The Service uses cloud computing infrastructure providers. The facilities are Google App Engine and Google Cloud Firestore. The infrastructure providers are bound by explicit privacy policies. However, it is technically feasible that they read the data D transiting through the Service. Whether this would constitute a violation of their own policies is debatable on a case-by-case basis. 80 |

81 | 82 |

83 | Anonymous data may be gathered for statistics purpose. This data includes, and is not limited to: 84 |

    85 |
  • country and city of usage,
  • 86 |
  • size and type of transfered data D,
  • 87 |
  • actions made by the same User,
  • 88 |
  • software quality metrics, such as the timing performances of Service processings.
  • 89 |
90 |

91 | 92 |

Contact

93 | 94 |

95 | deleplace2015@gmail.com 96 |

97 | 98 | 107 | 108 | -------------------------------------------------------------------------------- /B3/term.go: -------------------------------------------------------------------------------- 1 | package hotmaze 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io/ioutil" 7 | "log" 8 | "net/http" 9 | "time" 10 | 11 | "cloud.google.com/go/storage" 12 | "github.com/google/uuid" 13 | "github.com/skip2/go-qrcode" 14 | ) 15 | 16 | // HandlerDirectUpload is used from a terminal with cURL. 17 | // It receives file data, produces an ASCII-art QR code in the response body, 18 | // and doesn't require a web browser in the source computer (sender-side). 19 | func (s *Server) HandlerDirectUpload(w http.ResponseWriter, r *http.Request) { 20 | filedata, err := ioutil.ReadAll(r.Body) 21 | if err != nil { 22 | log.Println("Reading request body:", err) 23 | http.Error(w, "Could not read request :(", http.StatusInternalServerError) 24 | return 25 | } 26 | if len(bytes.TrimSpace(filedata)) == 0 { 27 | log.Println("Empty request body") 28 | http.Error(w, s.usage(), http.StatusBadRequest) 29 | return 30 | } 31 | log.Println("Received resource of size", len(filedata)) 32 | var filename string 33 | if f := r.FormValue("filename"); f != "" { 34 | filename = f 35 | } 36 | 37 | // 38 | // Store the incoming file data into a new temp GCS object 39 | // 40 | 41 | fileUUID := uuid.New().String() 42 | objectName := "transit/" + fileUUID 43 | objectHandle := s.StorageClient.Bucket(s.StorageBucket).Object(objectName) 44 | fileWriter := objectHandle.NewWriter(r.Context()) 45 | n, err := fileWriter.Write(filedata) 46 | if err != nil { 47 | log.Printf("Writing %d bytes to object %q in bucket %q: %v", len(filedata), objectName, s.StorageBucket, err) 48 | http.Error(w, "Could not write file to GCS :(", http.StatusInternalServerError) 49 | return 50 | } 51 | if n != len(filedata) { 52 | log.Printf("Wrote only %d / %d bytes to object %q in bucket %q: %v", n, len(filedata), objectName, s.StorageBucket, err) 53 | http.Error(w, "Could not write file to GCS :(", http.StatusInternalServerError) 54 | return 55 | } 56 | err = fileWriter.Close() 57 | if err != nil { 58 | log.Printf("Writing %d bytes to object %q in bucket %q: %v", len(filedata), objectName, s.StorageBucket, err) 59 | http.Error(w, "Could not write file to GCS :(", http.StatusInternalServerError) 60 | return 61 | } 62 | if filename != "" { 63 | _, err := objectHandle.Update(r.Context(), storage.ObjectAttrsToUpdate{ 64 | ContentDisposition: `filename="` + filename + `"`, 65 | }) 66 | if err != nil { 67 | log.Printf("Writing filename %q to object %q in bucket %q: %v", filename, objectName, s.StorageBucket, err) 68 | http.Error(w, "Could not write file to GCS :(", http.StatusInternalServerError) 69 | return 70 | } 71 | } 72 | log.Printf("Written object %q in bucket %q: %v", objectName, s.StorageBucket) 73 | getURL := s.BackendBaseURL + "/get/" + fileUUID 74 | 75 | tsched := time.Now() 76 | _, err = s.ScheduleForgetFile(r.Context(), fileUUID) 77 | if err != nil { 78 | log.Printf("Scheduling deletion of object %q in bucket %q: %v", objectName, s.StorageBucket, err) 79 | } 80 | log.Printf("Scheduled deletion of object %q in bucket %q", objectName, s.StorageBucket) 81 | 82 | // 83 | // Provide the access URL to the client in QR code + in clear text 84 | // 85 | 86 | qr, err := qrcode.New(getURL, qrcode.Medium) 87 | if err != nil { 88 | log.Println("Generating QR code:", err) 89 | http.Error(w, "Could not generate QR code :(", http.StatusInternalServerError) 90 | return 91 | } 92 | qrArt := qr2CharBlocks(qr) 93 | 94 | fmt.Fprintln(w, qrArt) 95 | fmt.Fprintf(w, "\nPlease find your file at %s\n", getURL) 96 | 97 | tDelete := tsched.Add(s.StorageFileTTL) 98 | hDelete := tDelete.Format("15:04 MST") 99 | fmt.Fprintf(w, "\nIt is valid until %s\n", hDelete) 100 | } 101 | 102 | func (s *Server) usage() string { 103 | return fmt.Sprintf(` 104 | Welcome to Hot Maze in command-line mode 105 | 106 | Usage: 107 | curl --data-binary @my_file.png ` + s.BackendBaseURL + `/term 108 | `) 109 | } 110 | 111 | // QRCode object -> ASCII-art string image of the QR code. 112 | // Makes sense only with fixed-width fonts. 113 | // The filled block character '█' is technically Unicode, not ASCII. 114 | func qr2CharBlocks(qr *qrcode.QRCode) string { 115 | bits := qr.Bitmap() 116 | var buf bytes.Buffer 117 | for y := range bits { 118 | buf.WriteString(" ") 119 | for x := range bits[y] { 120 | if bits[y][x] { 121 | // Blank square 122 | buf.WriteString(" ") 123 | } else { 124 | // Filled square 125 | buf.WriteString("██") 126 | } 127 | } 128 | buf.WriteString("\n") 129 | } 130 | return buf.String() 131 | } 132 | -------------------------------------------------------------------------------- /A00/web/static/js/hot-maze.js: -------------------------------------------------------------------------------- 1 | 2 | // Those are a few hundred lines of highly questionable JS. 3 | // They are not representative of state-of-the-art frontend practices. 4 | 5 | 6 | // Workflow A00: put the data in the QR-code. No cloud. 7 | // 8 | // 1) The page displays a File input/Drag'n'drop zone 9 | // 2) The User drops a file F 10 | // 4) The page displays a QR-code containing the data of F 11 | // 5) The user scans the QR-code with their mobile 12 | // 6) The mobile downloads F 13 | 14 | let resourceFile; 15 | let qrText; 16 | 17 | function showError(errmsg) { 18 | console.log("Error: " + errmsg) 19 | clearQR(); 20 | freezeResize(); 21 | errorZone.innerHTML = errmsg; 22 | errorZone.style.display = "block"; 23 | return false; 24 | } 25 | 26 | let qrHundredth = 40; 27 | 28 | function render(colorDark, clickCallback){ 29 | // qrHundredth == 95 is a "big QR-code" (almost fullscreen) 30 | // qrHundredth == 50 is a "medium-size QR-code" 31 | 32 | // -50 is for the small [?] box on the left 33 | var w = Math.max(document.documentElement.clientWidth -50, (window.innerWidth || 0) -50) * qrHundredth / 100; 34 | var h = Math.max(document.documentElement.clientHeight, window.innerHeight || 0) * qrHundredth / 100; 35 | var c = 400; 36 | if ( w>0 ) 37 | c = w; 38 | if ( h>0 && h bigger QR 67 | var resizeTimeOut = null; 68 | window.onresize = function(){ 69 | if (resizeTimeOut != null) 70 | clearTimeout(resizeTimeOut); 71 | resizeTimeOut = setTimeout(function(){ 72 | render("black", embiggen); 73 | }, 300); 74 | }; 75 | function freezeResize(){ 76 | // after scan, or on fatal error, we don't need/want to 77 | // redisplay a fresh black QR-code again. 78 | window.onresize = function(){ return false; } 79 | } 80 | 81 | function clearQR() { 82 | var node = qrcode; 83 | while (node.firstChild) { 84 | node.removeChild(node.firstChild); 85 | } 86 | } 87 | 88 | questionMark.onclick = function(event) { 89 | if ( helpContents.style.display === "none" ){ 90 | helpContents.style.display = "block"; 91 | questionMark.style.display = "none"; 92 | } 93 | } 94 | helpClose.onclick = function(event) { 95 | helpContents.style.display = "none"; 96 | questionMark.style.display = ""; 97 | } 98 | document.onkeydown = function(evt) { 99 | evt = evt || window.event; 100 | var isEscape = false; 101 | if ("key" in evt) { 102 | isEscape = (evt.key == "Escape" || evt.key == "Esc"); 103 | } else { 104 | isEscape = (evt.keyCode == 27); 105 | } 106 | if (isEscape) { 107 | questionMark.style.display = ""; 108 | helpContents.style.display = "none"; 109 | } 110 | }; 111 | 112 | function embiggen(event) { 113 | // Change size on each click 114 | // 40, 68, 96, 40, 60, 96 ... 115 | qrHundredth += 28; 116 | if ( qrHundredth>100 ) 117 | qrHundredth = 40; 118 | render("black", embiggen); 119 | } 120 | 121 | function displayGetQrCode() { 122 | resourceFile.text().then(text => { 123 | const limit = 2328; // bytes 124 | if ( text.length > limit ) { 125 | showError(`Can't encode ${resourceFile.size} bytes in QR-code, max is ${limit}`); 126 | return; 127 | } 128 | qrText = text; 129 | render("black", embiggen); 130 | }); 131 | } 132 | 133 | function processResourceFile() { 134 | // Here, resourceFile has been set either through the file select input, 135 | // or through a drag'n'drop. 136 | // What happens next with resourceFile is the same in both use cases. 137 | 138 | formZone.style.display = "none"; 139 | displayGetQrCode(); 140 | } 141 | 142 | resourceInput.onchange = function(e) { 143 | resourceFile = resourceInput.files[0]; 144 | processResourceFile(); 145 | }; 146 | 147 | function handleDnd(){ 148 | // See https://css-tricks.com/drag-and-drop-file-uploading/ 149 | var div = document.createElement('div'); 150 | var dndFileSupported = (('draggable' in div) 151 | || ('ondragstart' in div && 'ondrop' in div)) 152 | && 'FormData' in window && 'FileReader' in window; 153 | if (!dndFileSupported) 154 | return; 155 | 156 | dropInvite.style.display = "block"; 157 | 158 | var dropZone = resourceUpload; 159 | 160 | function highlightDropZone(){ 161 | dropZone.style.border = 'red dotted 10px'; 162 | } 163 | 164 | function unhighlightDropZone(){ 165 | dropZone.style.border = 'rgba(255, 255, 255, 0) solid 10px'; 166 | } 167 | 168 | dropZone.addEventListener('dragover', function(evt){ 169 | evt.stopPropagation(); 170 | evt.preventDefault(); 171 | highlightDropZone() 172 | evt.dataTransfer.dropEffect = 'copy'; 173 | }, false); 174 | 175 | dropZone.addEventListener('drop', function(evt){ 176 | evt.stopPropagation(); 177 | evt.preventDefault(); 178 | helpContents.style.display = "none"; 179 | unhighlightDropZone(); 180 | 181 | var files = evt.dataTransfer.files; 182 | if( files.length>1 ){ 183 | showError("Multiple upload not supported (yet)"); 184 | return; 185 | } 186 | 187 | // Upload 1st file only. 188 | resourceFile = files[0]; 189 | processResourceFile(); 190 | }, false); 191 | 192 | dropZone.addEventListener('dragleave', function(evt){ 193 | unhighlightDropZone(); 194 | }, false); 195 | } 196 | 197 | // 198 | // Let's start: init, then wait for user file selection. 199 | // 200 | 201 | handleDnd(); 202 | -------------------------------------------------------------------------------- /template/web/static/js/hot-maze.js: -------------------------------------------------------------------------------- 1 | 2 | // Those are a few hundred lines of highly questionable JS. 3 | // They are not representative of state-of-the-art frontend practices. 4 | 5 | 6 | // Simplified workflow, to be adapted depending on the cloud 7 | // implementation: 8 | // 9 | // 1) The page displays a File input/Drag'n'drop zone 10 | // 2) The User drops a file F 11 | // 3) F is uploaded to the cloud 12 | // 4) The page displays a QR-code containing a URL for F 13 | // 5) The user scans the QR-code with their mobile 14 | // 6) The mobile downloads F 15 | 16 | let backend = "https://TODO"; 17 | 18 | let resourceFile; 19 | let qrText; 20 | 21 | function showError(errmsg) { 22 | console.log("Error: " + errmsg) 23 | clearQR(); 24 | freezeResize(); 25 | errorZone.innerHTML = errmsg; 26 | return false; 27 | } 28 | 29 | let qrHundredth = 40; 30 | 31 | function render(colorDark, clickCallback){ 32 | // qrHundredth == 95 is a "big QR-code" (almost fullscreen) 33 | // qrHundredth == 50 is a "medium-size QR-code" 34 | 35 | // -50 is for the small [?] box on the left 36 | var w = Math.max(document.documentElement.clientWidth -50, (window.innerWidth || 0) -50) * qrHundredth / 100; 37 | var h = Math.max(document.documentElement.clientHeight, window.innerHeight || 0) * qrHundredth / 100; 38 | var c = 400; 39 | if ( w>0 ) 40 | c = w; 41 | if ( h>0 && h bigger QR 61 | var resizeTimeOut = null; 62 | window.onresize = function(){ 63 | if (resizeTimeOut != null) 64 | clearTimeout(resizeTimeOut); 65 | resizeTimeOut = setTimeout(function(){ 66 | render("black", embiggen); 67 | }, 300); 68 | }; 69 | function freezeResize(){ 70 | // after scan, or on fatal error, we don't need/want to 71 | // redisplay a fresh black QR-code again. 72 | window.onresize = function(){ return false; } 73 | } 74 | 75 | function clearQR() { 76 | var node = qrcode; 77 | while (node.firstChild) { 78 | node.removeChild(node.firstChild); 79 | } 80 | } 81 | 82 | questionMark.onclick = function(event) { 83 | if ( helpContents.style.display === "none" ){ 84 | helpContents.style.display = "block"; 85 | questionMark.style.display = "none"; 86 | } 87 | } 88 | helpClose.onclick = function(event) { 89 | helpContents.style.display = "none"; 90 | questionMark.style.display = ""; 91 | } 92 | document.onkeydown = function(evt) { 93 | evt = evt || window.event; 94 | var isEscape = false; 95 | if ("key" in evt) { 96 | isEscape = (evt.key == "Escape" || evt.key == "Esc"); 97 | } else { 98 | isEscape = (evt.keyCode == 27); 99 | } 100 | if (isEscape) { 101 | questionMark.style.display = ""; 102 | helpContents.style.display = "none"; 103 | } 104 | }; 105 | 106 | function embiggen(event) { 107 | // Change size on each click 108 | // 40, 68, 96, 40, 60, 96 ... 109 | qrHundredth += 28; 110 | if ( qrHundredth>100 ) 111 | qrHundredth = 40; 112 | render("black", embiggen); 113 | } 114 | 115 | window.addEventListener('offline', function(){ 116 | showError("Lost internet connectivity :(") 117 | }); 118 | 119 | 120 | function checkInternetAndExecute(action){ 121 | action(); 122 | return; 123 | // TODO implement the check below, at a proper endpoint 124 | 125 | // This request is supposed to be fast. 126 | var tinyRequest = new XMLHttpRequest(); 127 | tinyRequest.onreadystatechange = function() { 128 | if (tinyRequest.readyState == 4 ) { 129 | if (tinyRequest.status == 200) { 130 | // Response ensures we have internet connectivity 131 | action(); 132 | } else { 133 | showError("No internet...?"); 134 | } 135 | } 136 | }; 137 | tinyRequest.open("GET", backend+"/online", true); 138 | tinyRequest.send( null ); 139 | } 140 | 141 | function doUpload(action) { 142 | uploadProgress.value = 0.0; 143 | 144 | // This is a fake upload. 145 | // TODO: Replace with actual upload to cloud. 146 | let uploadTime = (0.5 + 2*Math.random()) * 1000; 147 | let incr = 100; 148 | let t = 0; 149 | let nextStep; 150 | nextStep = function() { 151 | setTimeout(function() { 152 | t += incr; 153 | let ratio = Math.min(t/uploadTime, 1.0); 154 | uploadProgress.value = ratio; 155 | if(t>=uploadTime) { 156 | setTimeout(function() { 157 | formZone.style.display = "none"; 158 | uploadProgress.style.display = "none"; 159 | action(); 160 | }, 380); 161 | return; 162 | } 163 | nextStep(); 164 | }, incr) 165 | } 166 | nextStep(); 167 | } 168 | 169 | function displayGetQrCode() { 170 | // This is a fake download URL. 171 | // TODO: replace with fresh URL to cloud file location. 172 | qrText = "never gonna give you up never gonna let you down"; 173 | render("black", embiggen); 174 | } 175 | 176 | function processResourceFile() { 177 | // Here, resourceFile has been set either through the file select input, 178 | // or through a drag'n'drop. 179 | // What happens next with resourceFile is the same in both use cases. 180 | 181 | fileForm.style.display = "none"; 182 | uploadProgress.style.display = "inline"; 183 | resourceUpload.classList.add("transfering"); 184 | doUpload( displayGetQrCode ); 185 | } 186 | 187 | resourceInput.onchange = function(e) { 188 | resourceFile = resourceInput.files[0]; 189 | processResourceFile(); 190 | }; 191 | 192 | function handleDnd(){ 193 | // See https://css-tricks.com/drag-and-drop-file-uploading/ 194 | var div = document.createElement('div'); 195 | var dndFileSupported = (('draggable' in div) 196 | || ('ondragstart' in div && 'ondrop' in div)) 197 | && 'FormData' in window && 'FileReader' in window; 198 | if (!dndFileSupported) 199 | return; 200 | 201 | dropInvite.style.display = "block"; 202 | 203 | var dropZone = resourceUpload; 204 | 205 | function highlightDropZone(){ 206 | dropZone.style.border = 'red dotted 10px'; 207 | } 208 | 209 | function unhighlightDropZone(){ 210 | dropZone.style.border = 'rgba(255, 255, 255, 0) solid 10px'; 211 | } 212 | 213 | dropZone.addEventListener('dragover', function(evt){ 214 | evt.stopPropagation(); 215 | evt.preventDefault(); 216 | highlightDropZone() 217 | evt.dataTransfer.dropEffect = 'copy'; 218 | }, false); 219 | 220 | dropZone.addEventListener('drop', function(evt){ 221 | evt.stopPropagation(); 222 | evt.preventDefault(); 223 | helpContents.style.display = "none"; 224 | unhighlightDropZone(); 225 | 226 | var files = evt.dataTransfer.files; 227 | if( files.length>1 ){ 228 | showError("Multiple upload not supported (yet)"); 229 | return; 230 | } 231 | 232 | // Upload 1st file only. 233 | resourceFile = files[0]; 234 | processResourceFile(); 235 | }, false); 236 | 237 | dropZone.addEventListener('dragleave', function(evt){ 238 | unhighlightDropZone(); 239 | }, false); 240 | } 241 | 242 | // 243 | // Let's start: init, then wait for user file selection. 244 | // 245 | 246 | handleDnd(); 247 | 248 | checkInternetAndExecute(function(){ 249 | // Unlock upload widgets, only if we do have internet! 250 | formZone.style.display = "block"; 251 | }); --------------------------------------------------------------------------------