├── 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 | [](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 | [](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 | [](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 | [](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 | [](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 | [](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 | [](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 |
16 |
🗙
17 |
18 |
You can share a document from your computer to your mobile in seconds
19 |
20 | Drop a file on this page
21 | Scan the QR-code with your mobile
22 |
23 |
Your mobile device must have a QR-code reader (get one for Android , or for iOS ).
24 |
The two devices (source and target) must have an internet connection.
25 |
Your data remains private . It is not publicly available and not disclosed to third parties. See the Privacy terms .
26 |
27 |
28 | But I want to send from mobile to desktop instead! Use
Cool Maze .
29 |
30 |
31 |
32 |
33 |
41 |
42 |
46 |
47 |
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 |
16 |
🗙
17 |
18 |
You can share a document from your computer to your mobile in seconds
19 |
20 |
21 | Drop a file on this page
22 | Scan the QR-code with your mobile
23 |
24 |
25 |
That's it. No login, no passwords, no ads.
26 |
Your mobile device must have a QR-code reader (get one for Android , or for iOS ).
27 |
The two devices (source and target) must have an internet connection.
28 |
Your data remains private . It is not publicly available and not disclosed to third parties. See the Privacy terms .
29 |
30 |
31 | But I want to send from mobile to desktop instead! Use
Cool Maze .
32 |
33 |
34 |
35 |
36 |
44 |
45 |
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 |
16 |
✖
17 |
18 |
You can share a document from your computer to your mobile in seconds
19 |
20 | Drop a file on this page
21 | Scan the QR-code with your mobile
22 |
23 |
Your mobile device must have a QR-code reader (get one for Android , or for iOS ).
24 |
The two devices (source and target) must have an internet connection.
25 |
Your data remains private . It is not publicly available and not disclosed to third parties. See the Privacy terms .
26 |
27 |
28 | But I want to send from mobile to desktop instead! Use
Cool Maze .
29 |
30 |
31 |
32 |
33 |
41 |
42 |
46 |
47 |
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 |
16 |
🗙
17 |
18 |
You can share a document from your computer to your mobile in seconds
19 |
20 | Drop a file on this page
21 | Scan the QR-code with your mobile
22 |
23 |
Your mobile device must have a QR-code reader (get one for Android , or for iOS ).
24 |
The two devices (source and target) must have an internet connection.
25 |
Your data remains private . It is not publicly available and not disclosed to third parties. See the Privacy terms .
26 |
27 |
28 | But I want to send from mobile to desktop instead! Use
Cool Maze .
29 |
30 |
31 |
32 |
33 |
41 |
42 |
46 |
47 |
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 |
16 |
🗙
17 |
18 |
You can share a document from your computer to your mobile in seconds
19 |
20 | Drop a file on this page
21 | Scan the QR-code with your mobile
22 |
23 |
Your mobile device must have a QR-code reader (get one for Android , or for iOS ).
24 |
The two devices (source and target) must have an internet connection.
25 |
Your data remains private . It is not publicly available and not disclosed to third parties. See the Privacy terms .
26 |
27 |
28 | But I want to send from mobile to desktop instead! Use
Cool Maze .
29 |
30 |
31 |
32 |
33 |
41 |
42 |
46 |
47 |
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 |
16 |
🗙
17 |
18 |
You can share a document from your computer to your mobile in seconds
19 |
20 | Drop a file on this page
21 | Scan the QR-code with your mobile
22 |
23 |
Your mobile device must have a QR-code reader (get one for Android , or for iOS ).
24 |
The two devices (source and target) must have an internet connection.
25 |
Your data remains private . It is not publicly available and not disclosed to third parties. See the Privacy terms .
26 |
27 |
28 | But I want to send from mobile to desktop instead! Use
Cool Maze .
29 |
30 |
31 |
32 |
33 |
41 |
42 |
46 |
47 |
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 |
16 |
🗙
17 |
18 |
You can share a document from your computer to your mobile in seconds
19 |
20 |
21 | Drop a file on this page
22 | Scan the QR-code with your mobile
23 |
24 |
25 |
That's it. No login, no passwords, no ads.
26 |
Your mobile device must have a QR-code reader (get one for Android , or for iOS ).
27 |
The two devices (source and target) must have an internet connection.
28 |
Your data remains private . It is not publicly available and not disclosed to third parties. See the Privacy terms .
29 |
30 |
31 | But I want to send from mobile to desktop instead! Use
Cool Maze .
32 |
33 |
34 |
35 |
36 |
44 |
45 |
49 |
50 |
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 | });
--------------------------------------------------------------------------------