From 0ad912fab44f32fcaa5ea719607d227af1903ac3 Mon Sep 17 00:00:00 2001 From: Ambrose Chua Date: Sat, 13 Jun 2020 14:49:35 +0800 Subject: [PATCH] WIP: Initial web gallery --- README.md | 2 + cmd/proxy/proxy.go | 2 +- cmd/web/templates.go | 1 + cmd/web/web.go | 1 + web/manage/.gitignore | 8 +- web/manage/Makefile | 51 ++-- web/manage/index.tmpl | 7 +- web/manage/indextmpl.go | 6 +- web/manage/package.json | 2 +- web/manage/rollup.config.js | 6 +- web/manage/src/App.svelte | 46 ++-- web/manage/src/Gallery.svelte | 13 + web/manage/src/Photo.svelte | 33 +++ web/manage/src/main.js | 2 +- web/shared/css/manage.css | 3 + web/shared/js/manage.js | 442 ++++++++++++++++++++++++++++++++++ 16 files changed, 561 insertions(+), 64 deletions(-) create mode 100644 web/manage/src/Gallery.svelte create mode 100644 web/manage/src/Photo.svelte create mode 100644 web/shared/css/manage.css create mode 100644 web/shared/js/manage.js diff --git a/README.md b/README.md index 4de087d..6e824a1 100644 --- a/README.md +++ b/README.md @@ -88,4 +88,6 @@ Generate previews from photo buckets. Registers webhooks. Reverse proxies buckets to the minio endpoint, as a substitute for the AWS S3 website hosting features. Serves up `index.html` when URLs end in a slash. +In production, [replace this with Nginx](https://docs.min.io/docs/setup-nginx-proxy-with-minio). + diff --git a/cmd/proxy/proxy.go b/cmd/proxy/proxy.go index 3f1a754..c468d7c 100644 --- a/cmd/proxy/proxy.go +++ b/cmd/proxy/proxy.go @@ -62,7 +62,7 @@ func director(req *http.Request) { } else { // If we are using subdomains, set the host header req.URL.Path = mapPath(*req) - //req.Header.Set("Host", host) + req.Header.Set("Host", host) } // Prevent MINIO from issuing redirects diff --git a/cmd/web/templates.go b/cmd/web/templates.go index 4ee7830..1b105d1 100644 --- a/cmd/web/templates.go +++ b/cmd/web/templates.go @@ -16,6 +16,7 @@ var funcs = template.FuncMap{ type IndexTemplateData struct { Assets string + Bucket string Metadata lib.BucketMetadata Photos []Photo } diff --git a/cmd/web/web.go b/cmd/web/web.go index 91de486..d0e5d86 100644 --- a/cmd/web/web.go +++ b/cmd/web/web.go @@ -85,6 +85,7 @@ func update(w http.ResponseWriter, req *http.Request) { data := IndexTemplateData{ Assets: endpoint + "/shared/", + Bucket: bucket, Metadata: metadata, Photos: detailedPhotos, } diff --git a/web/manage/.gitignore b/web/manage/.gitignore index da93220..3ea2be4 100644 --- a/web/manage/.gitignore +++ b/web/manage/.gitignore @@ -1,4 +1,6 @@ -/node_modules/ -/public/build/ - .DS_Store + +/node_modules/ +/build/ + +.container diff --git a/web/manage/Makefile b/web/manage/Makefile index 8d2999f..ec7be76 100644 --- a/web/manage/Makefile +++ b/web/manage/Makefile @@ -1,64 +1,71 @@ -DOCKER ?= podman - NAME ?= photos-web-manage -OUTPUT ?= public/build -SOURCE ?= src -ARG_PUBLISH ?= --publish 5000:5000 +DOCKER ?= podman +GO ?= go +OUTPUT ?= build +SOURCE ?= src + +DEV_PORT ?= 5000 SHARED ?= ../shared -shared_copy = public/shared +SHARED_COPY ?= $(OUTPUT)/shared current_dir = $(shell pwd) -shared_source = $(wildcard $(SHARED)/*/*) -shared_target = $(foreach path,$(shared_source),$(subst $(SHARED),$(shared_copy),$(path))) +shared_files = $(wildcard $(SHARED)/*/*) +shared_copy_files = $(foreach path,$(shared_files),$(subst $(SHARED)/,$(SHARED_COPY)/,$(path))) +container_name = $(NAME)-builder container_workdir = /working arg_base = \ --rm --interactive --tty \ --workdir $(container_workdir) \ --security-opt label=disable +arg_publish = \ + --publish $(DEV_PORT):$(DEV_PORT) arg_volume = \ - -v $(current_dir)/public:$(container_workdir)/public \ - -v $(current_dir)/src:$(container_workdir)/src \ + -v $(current_dir)/$(OUTPUT):$(container_workdir)/$(OUTPUT) \ + -v $(current_dir)/$(SOURCE):$(container_workdir)/$(SOURCE) \ .PHONY: default default: build install .PHONY: clean clean: - $(RM) -r $(shared_copy) $(OUTPUT) public/ .container + $(RM) -r $(OUTPUT) .container .PHONY: container .containertest container: .containertest .container .containertest: - $(DOCKER) image inspect $(NAME)-build 2>&1 >/dev/null || rm .container || exit 0 + $(DOCKER) image inspect $(container_name) 2>&1 >/dev/null || rm .container || exit 0 .container: Dockerfile package.json rollup.config.js - $(DOCKER) build --tag $(NAME)-build --no-cache . + $(DOCKER) build --tag $(container_name) --no-cache . touch $@ +DEV_BUCKET ?= test.ambrose.photos .PHONY: dev # Start a development server -dev: public/index.html $(shared_target) | container - $(DOCKER) run $(arg_base) $(ARG_PUBLISH) $(arg_volume) \ - $(NAME)-build +dev: $(SOURCE)/* $(OUTPUT)/index.html $(shared_copy_files) | container + echo "$(shared_copy_files)" + $(DOCKER) run $(arg_base) $(arg_publish) $(arg_volume) \ + $(container_name) -public/index.html: index.tmpl +$(OUTPUT)/index.html: index.tmpl indextmpl.go mkdir -p $(@D) - go run indextmpl.go -t $? -o $@ -a $(subst public/,,$(shared_copy))/ -b $(subst public/,,$(OUTPUT))/ -mt "[dev] Manage" + $(GO) run indextmpl.go -t $< -o $@ -b $(DEV_BUCKET) -a $(subst $(OUTPUT)/,,$(SHARED_COPY))/ -mt "[dev] Manage" .SECONDEXPANSION: -$(shared_target): $$(subst $$(shared_copy),$$(SHARED),$$@) +$(shared_copy_files): $$(subst $$(SHARED_COPY)/,$$(SHARED)/,$$@) mkdir -p $(@D) && cp $? $@ .PHONY: build install -# Build with rollup +# Build with rollup build: $(OUTPUT)/manage.js -$(OUTPUT)/manage.js: src/* | container +$(OUTPUT)/manage.js: $(SOURCE)/* | container + mkdir -p $(@D) $(DOCKER) run $(arg_base) $(arg_volume) \ - $(NAME)-build npm run build + $(container_name) npm run build install: build cp $(OUTPUT)/manage.js $(SHARED)/js/manage.js cp $(OUTPUT)/manage.css $(SHARED)/css/manage.css diff --git a/web/manage/index.tmpl b/web/manage/index.tmpl index 86f91e4..ad5e509 100644 --- a/web/manage/index.tmpl +++ b/web/manage/index.tmpl @@ -5,11 +5,10 @@ {{ .Metadata.Title }} - + - + - - + diff --git a/web/manage/indextmpl.go b/web/manage/indextmpl.go index 00d155d..1df77b7 100644 --- a/web/manage/indextmpl.go +++ b/web/manage/indextmpl.go @@ -10,8 +10,8 @@ import ( type IndexData struct { Assets string + Bucket string Development bool - Build string Metadata Metadata } @@ -28,8 +28,8 @@ var out string func main() { flag.StringVar(&tmpl, "t", "", "template file") flag.StringVar(&out, "o", "", "output file") + flag.StringVar(&data.Bucket, "b", "", ".Bucket") flag.StringVar(&data.Assets, "a", "", ".Assets") - flag.StringVar(&data.Build, "b", "", ".Build") flag.StringVar(&data.Metadata.Title, "mt", "", ".Metadata.Title") flag.Parse() @@ -52,7 +52,7 @@ func main() { panic(err) } - of, err := os.OpenFile(out, os.O_WRONLY|os.O_CREATE, 0644) + of, err := os.OpenFile(out, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) if err != nil { panic(err) } diff --git a/web/manage/package.json b/web/manage/package.json index d945b6e..3c73e16 100644 --- a/web/manage/package.json +++ b/web/manage/package.json @@ -4,7 +4,7 @@ "scripts": { "build": "rollup -c", "dev": "rollup -c -w", - "start": "sirv public --host 0.0.0.0" + "start": "sirv build --host 0.0.0.0" }, "devDependencies": { "@rollup/plugin-commonjs": "^12.0.0", diff --git a/web/manage/rollup.config.js b/web/manage/rollup.config.js index 3649681..64d12e9 100644 --- a/web/manage/rollup.config.js +++ b/web/manage/rollup.config.js @@ -12,7 +12,7 @@ export default { sourcemap: true, format: 'iife', name: 'app', - file: 'public/build/manage.js' + file: 'build/manage.js' }, plugins: [ svelte({ @@ -21,7 +21,7 @@ export default { // we'll extract any component CSS out into // a separate file - better for performance css: css => { - css.write('public/build/manage.css'); + css.write('build/manage.css'); } }), @@ -42,7 +42,7 @@ export default { // Watch the `public` directory and refresh the // browser on changes when not in production - !production && livereload('public'), + !production && livereload('build'), // If we're building for production (npm run build // instead of npm run dev), minify diff --git a/web/manage/src/App.svelte b/web/manage/src/App.svelte index 10faec7..a8e864e 100644 --- a/web/manage/src/App.svelte +++ b/web/manage/src/App.svelte @@ -1,30 +1,24 @@ -
-

Hello {name}!

-

Visit the Svelte tutorial to learn how to build Svelte apps.

-
+
+ +
+

{bucket}

+
+ +
- \ No newline at end of file + diff --git a/web/manage/src/Gallery.svelte b/web/manage/src/Gallery.svelte new file mode 100644 index 0000000..fc2ffd3 --- /dev/null +++ b/web/manage/src/Gallery.svelte @@ -0,0 +1,13 @@ + + +
+ {#each photos as photo} + + {/each} +
+ + diff --git a/web/manage/src/Photo.svelte b/web/manage/src/Photo.svelte new file mode 100644 index 0000000..ba49c3d --- /dev/null +++ b/web/manage/src/Photo.svelte @@ -0,0 +1,33 @@ + + + + diff --git a/web/manage/src/main.js b/web/manage/src/main.js index 1fe1b22..7850902 100644 --- a/web/manage/src/main.js +++ b/web/manage/src/main.js @@ -3,7 +3,7 @@ import App from './App.svelte'; const app = new App({ target: document.body, props: { - name: 'ambrose' + bucket: document.body.dataset.bucket, } }); diff --git a/web/shared/css/manage.css b/web/shared/css/manage.css new file mode 100644 index 0000000..9a464b4 --- /dev/null +++ b/web/shared/css/manage.css @@ -0,0 +1,3 @@ +main.svelte-1tky8bj{text-align:center;padding:1em;max-width:240px;margin:0 auto}h1.svelte-1tky8bj{color:#ff3e00;text-transform:uppercase;font-size:4em;font-weight:100}@media(min-width: 640px){main.svelte-1tky8bj{max-width:none}} + +/*# sourceMappingURL=manage.css.map */ \ No newline at end of file diff --git a/web/shared/js/manage.js b/web/shared/js/manage.js new file mode 100644 index 0000000..4c2f8d0 --- /dev/null +++ b/web/shared/js/manage.js @@ -0,0 +1,442 @@ + +(function(l, r) { if (l.getElementById('livereloadscript')) return; r = l.createElement('script'); r.async = 1; r.src = '//' + (window.location.host || 'localhost').split(':')[0] + ':35729/livereload.js?snipver=1'; r.id = 'livereloadscript'; l.getElementsByTagName('head')[0].appendChild(r) })(window.document); +var app = (function () { + 'use strict'; + + function noop() { } + function add_location(element, file, line, column, char) { + element.__svelte_meta = { + loc: { file, line, column, char } + }; + } + function run(fn) { + return fn(); + } + function blank_object() { + return Object.create(null); + } + function run_all(fns) { + fns.forEach(run); + } + function is_function(thing) { + return typeof thing === 'function'; + } + function safe_not_equal(a, b) { + return a != a ? b == b : a !== b || ((a && typeof a === 'object') || typeof a === 'function'); + } + + function append(target, node) { + target.appendChild(node); + } + function insert(target, node, anchor) { + target.insertBefore(node, anchor || null); + } + function detach(node) { + node.parentNode.removeChild(node); + } + function element(name) { + return document.createElement(name); + } + function text(data) { + return document.createTextNode(data); + } + function space() { + return text(' '); + } + function attr(node, attribute, value) { + if (value == null) + node.removeAttribute(attribute); + else if (node.getAttribute(attribute) !== value) + node.setAttribute(attribute, value); + } + function children(element) { + return Array.from(element.childNodes); + } + function custom_event(type, detail) { + const e = document.createEvent('CustomEvent'); + e.initCustomEvent(type, false, false, detail); + return e; + } + + let current_component; + function set_current_component(component) { + current_component = component; + } + + const dirty_components = []; + const binding_callbacks = []; + const render_callbacks = []; + const flush_callbacks = []; + const resolved_promise = Promise.resolve(); + let update_scheduled = false; + function schedule_update() { + if (!update_scheduled) { + update_scheduled = true; + resolved_promise.then(flush); + } + } + function add_render_callback(fn) { + render_callbacks.push(fn); + } + let flushing = false; + const seen_callbacks = new Set(); + function flush() { + if (flushing) + return; + flushing = true; + do { + // first, call beforeUpdate functions + // and update components + for (let i = 0; i < dirty_components.length; i += 1) { + const component = dirty_components[i]; + set_current_component(component); + update(component.$$); + } + dirty_components.length = 0; + while (binding_callbacks.length) + binding_callbacks.pop()(); + // then, once components are updated, call + // afterUpdate functions. This may cause + // subsequent updates... + for (let i = 0; i < render_callbacks.length; i += 1) { + const callback = render_callbacks[i]; + if (!seen_callbacks.has(callback)) { + // ...so guard against infinite loops + seen_callbacks.add(callback); + callback(); + } + } + render_callbacks.length = 0; + } while (dirty_components.length); + while (flush_callbacks.length) { + flush_callbacks.pop()(); + } + update_scheduled = false; + flushing = false; + seen_callbacks.clear(); + } + function update($$) { + if ($$.fragment !== null) { + $$.update(); + run_all($$.before_update); + const dirty = $$.dirty; + $$.dirty = [-1]; + $$.fragment && $$.fragment.p($$.ctx, dirty); + $$.after_update.forEach(add_render_callback); + } + } + const outroing = new Set(); + function transition_in(block, local) { + if (block && block.i) { + outroing.delete(block); + block.i(local); + } + } + function mount_component(component, target, anchor) { + const { fragment, on_mount, on_destroy, after_update } = component.$$; + fragment && fragment.m(target, anchor); + // onMount happens before the initial afterUpdate + add_render_callback(() => { + const new_on_destroy = on_mount.map(run).filter(is_function); + if (on_destroy) { + on_destroy.push(...new_on_destroy); + } + else { + // Edge case - component was destroyed immediately, + // most likely as a result of a binding initialising + run_all(new_on_destroy); + } + component.$$.on_mount = []; + }); + after_update.forEach(add_render_callback); + } + function destroy_component(component, detaching) { + const $$ = component.$$; + if ($$.fragment !== null) { + run_all($$.on_destroy); + $$.fragment && $$.fragment.d(detaching); + // TODO null out other refs, including component.$$ (but need to + // preserve final state?) + $$.on_destroy = $$.fragment = null; + $$.ctx = []; + } + } + function make_dirty(component, i) { + if (component.$$.dirty[0] === -1) { + dirty_components.push(component); + schedule_update(); + component.$$.dirty.fill(0); + } + component.$$.dirty[(i / 31) | 0] |= (1 << (i % 31)); + } + function init(component, options, instance, create_fragment, not_equal, props, dirty = [-1]) { + const parent_component = current_component; + set_current_component(component); + const prop_values = options.props || {}; + const $$ = component.$$ = { + fragment: null, + ctx: null, + // state + props, + update: noop, + not_equal, + bound: blank_object(), + // lifecycle + on_mount: [], + on_destroy: [], + before_update: [], + after_update: [], + context: new Map(parent_component ? parent_component.$$.context : []), + // everything else + callbacks: blank_object(), + dirty + }; + let ready = false; + $$.ctx = instance + ? instance(component, prop_values, (i, ret, ...rest) => { + const value = rest.length ? rest[0] : ret; + if ($$.ctx && not_equal($$.ctx[i], $$.ctx[i] = value)) { + if ($$.bound[i]) + $$.bound[i](value); + if (ready) + make_dirty(component, i); + } + return ret; + }) + : []; + $$.update(); + ready = true; + run_all($$.before_update); + // `false` as a special case of no DOM component + $$.fragment = create_fragment ? create_fragment($$.ctx) : false; + if (options.target) { + if (options.hydrate) { + const nodes = children(options.target); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + $$.fragment && $$.fragment.l(nodes); + nodes.forEach(detach); + } + else { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + $$.fragment && $$.fragment.c(); + } + if (options.intro) + transition_in(component.$$.fragment); + mount_component(component, options.target, options.anchor); + flush(); + } + set_current_component(parent_component); + } + class SvelteComponent { + $destroy() { + destroy_component(this, 1); + this.$destroy = noop; + } + $on(type, callback) { + const callbacks = (this.$$.callbacks[type] || (this.$$.callbacks[type] = [])); + callbacks.push(callback); + return () => { + const index = callbacks.indexOf(callback); + if (index !== -1) + callbacks.splice(index, 1); + }; + } + $set() { + // overridden by instance, if it has props + } + } + + function dispatch_dev(type, detail) { + document.dispatchEvent(custom_event(type, Object.assign({ version: '3.23.1' }, detail))); + } + function append_dev(target, node) { + dispatch_dev("SvelteDOMInsert", { target, node }); + append(target, node); + } + function insert_dev(target, node, anchor) { + dispatch_dev("SvelteDOMInsert", { target, node, anchor }); + insert(target, node, anchor); + } + function detach_dev(node) { + dispatch_dev("SvelteDOMRemove", { node }); + detach(node); + } + function attr_dev(node, attribute, value) { + attr(node, attribute, value); + if (value == null) + dispatch_dev("SvelteDOMRemoveAttribute", { node, attribute }); + else + dispatch_dev("SvelteDOMSetAttribute", { node, attribute, value }); + } + function set_data_dev(text, data) { + data = '' + data; + if (text.data === data) + return; + dispatch_dev("SvelteDOMSetData", { node: text, data }); + text.data = data; + } + function validate_slots(name, slot, keys) { + for (const slot_key of Object.keys(slot)) { + if (!~keys.indexOf(slot_key)) { + console.warn(`<${name}> received an unexpected slot "${slot_key}".`); + } + } + } + class SvelteComponentDev extends SvelteComponent { + constructor(options) { + if (!options || (!options.target && !options.$$inline)) { + throw new Error(`'target' is a required option`); + } + super(); + } + $destroy() { + super.$destroy(); + this.$destroy = () => { + console.warn(`Component was already destroyed`); // eslint-disable-line no-console + }; + } + $capture_state() { } + $inject_state() { } + } + + /* src/App.svelte generated by Svelte v3.23.1 */ + + const file = "src/App.svelte"; + + function create_fragment(ctx) { + let main; + let h1; + let t0; + let t1; + let t2; + let t3; + let p; + let t4; + let a; + let t6; + + const block = { + c: function create() { + main = element("main"); + h1 = element("h1"); + t0 = text("Hello "); + t1 = text(/*bucket*/ ctx[0]); + t2 = text("!"); + t3 = space(); + p = element("p"); + t4 = text("Visit the "); + a = element("a"); + a.textContent = "Svelte tutorial"; + t6 = text(" to learn how to build Svelte apps."); + attr_dev(h1, "class", "svelte-1tky8bj"); + add_location(h1, file, 5, 1, 48); + attr_dev(a, "href", "https://svelte.dev/tutorial"); + add_location(a, file, 6, 14, 87); + add_location(p, file, 6, 1, 74); + attr_dev(main, "class", "svelte-1tky8bj"); + add_location(main, file, 4, 0, 40); + }, + l: function claim(nodes) { + throw new Error("options.hydrate only works if the component was compiled with the `hydratable: true` option"); + }, + m: function mount(target, anchor) { + insert_dev(target, main, anchor); + append_dev(main, h1); + append_dev(h1, t0); + append_dev(h1, t1); + append_dev(h1, t2); + append_dev(main, t3); + append_dev(main, p); + append_dev(p, t4); + append_dev(p, a); + append_dev(p, t6); + }, + p: function update(ctx, [dirty]) { + if (dirty & /*bucket*/ 1) set_data_dev(t1, /*bucket*/ ctx[0]); + }, + i: noop, + o: noop, + d: function destroy(detaching) { + if (detaching) detach_dev(main); + } + }; + + dispatch_dev("SvelteRegisterBlock", { + block, + id: create_fragment.name, + type: "component", + source: "", + ctx + }); + + return block; + } + + function instance($$self, $$props, $$invalidate) { + let { bucket } = $$props; + const writable_props = ["bucket"]; + + Object.keys($$props).forEach(key => { + if (!~writable_props.indexOf(key) && key.slice(0, 2) !== "$$") console.warn(` was created with unknown prop '${key}'`); + }); + + let { $$slots = {}, $$scope } = $$props; + validate_slots("App", $$slots, []); + + $$self.$set = $$props => { + if ("bucket" in $$props) $$invalidate(0, bucket = $$props.bucket); + }; + + $$self.$capture_state = () => ({ bucket }); + + $$self.$inject_state = $$props => { + if ("bucket" in $$props) $$invalidate(0, bucket = $$props.bucket); + }; + + if ($$props && "$$inject" in $$props) { + $$self.$inject_state($$props.$$inject); + } + + return [bucket]; + } + + class App extends SvelteComponentDev { + constructor(options) { + super(options); + init(this, options, instance, create_fragment, safe_not_equal, { bucket: 0 }); + + dispatch_dev("SvelteRegisterComponent", { + component: this, + tagName: "App", + options, + id: create_fragment.name + }); + + const { ctx } = this.$$; + const props = options.props || {}; + + if (/*bucket*/ ctx[0] === undefined && !("bucket" in props)) { + console.warn(" was created without expected prop 'bucket'"); + } + } + + get bucket() { + throw new Error(": Props cannot be read directly from the component instance unless compiling with 'accessors: true' or ''"); + } + + set bucket(value) { + throw new Error(": Props cannot be set directly on the component instance unless compiling with 'accessors: true' or ''"); + } + } + + const app = new App({ + target: document.body, + props: { + bucket: document.body.dataset.bucket, + } + }); + + return app; + +}()); +//# sourceMappingURL=manage.js.map