Add event hub & websocket for push notifications

Signed-off-by: Michael Mayer <michael@liquidbytes.net>
This commit is contained in:
Michael Mayer 2019-11-16 16:06:34 +01:00
parent 9b03cc4d6d
commit 65f084193e
21 changed files with 407 additions and 126 deletions

View file

@ -4404,39 +4404,6 @@
}
}
},
"engine.io-client": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-3.2.1.tgz",
"integrity": "sha512-y5AbkytWeM4jQr7m/koQLc5AxpRKC1hEVUb/s1FUAWEJq5AzJJ4NLvzuKPuxtDi5Mq755WuDvZ6Iv2rXj4PTzw==",
"requires": {
"component-emitter": "1.2.1",
"component-inherit": "0.0.3",
"debug": "~3.1.0",
"engine.io-parser": "~2.1.1",
"has-cors": "1.1.0",
"indexof": "0.0.1",
"parseqs": "0.0.5",
"parseuri": "0.0.5",
"ws": "~3.3.1",
"xmlhttprequest-ssl": "~1.5.4",
"yeast": "0.1.2"
},
"dependencies": {
"component-emitter": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz",
"integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY="
},
"debug": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
"integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
"requires": {
"ms": "2.0.0"
}
}
}
},
"engine.io-parser": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-2.1.3.tgz",
@ -6171,9 +6138,9 @@
}
},
"handlebars": {
"version": "4.5.1",
"resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.5.1.tgz",
"integrity": "sha512-C29UoFzHe9yM61lOsIlCE5/mQVGrnIOrOq7maQl76L7tYPCgC1og0Ajt6uWnX4ZTxBPnjw+CUvawphwCfJgUnA==",
"version": "4.5.2",
"resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.5.2.tgz",
"integrity": "sha512-29Zxv/cynYB7mkT1rVWQnV7mGX6v7H/miQ6dbEpYTKq5eJBN7PsRB+ViYJlcT6JINTSu4dVB9kOqEun78h6Exg==",
"requires": {
"neo-async": "^2.6.0",
"optimist": "^0.6.1",
@ -10713,42 +10680,6 @@
"socket.io-client": "2.1.1",
"socket.io-parser": "~3.2.0"
},
"dependencies": {
"debug": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
"integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
"requires": {
"ms": "2.0.0"
}
}
}
},
"socket.io-adapter": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-1.1.1.tgz",
"integrity": "sha1-KoBeihTWNyEk3ZFZrUUC+MsH8Gs="
},
"socket.io-client": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-2.1.1.tgz",
"integrity": "sha512-jxnFyhAuFxYfjqIgduQlhzqTcOEQSn+OHKVfAxWaNWa7ecP7xSNk2Dx/3UEsDcY7NcFafxvNvKPmmO7HTwTxGQ==",
"requires": {
"backo2": "1.0.2",
"base64-arraybuffer": "0.1.5",
"component-bind": "1.0.0",
"component-emitter": "1.2.1",
"debug": "~3.1.0",
"engine.io-client": "~3.2.0",
"has-binary2": "~1.0.2",
"has-cors": "1.1.0",
"indexof": "0.0.1",
"object-component": "0.0.3",
"parseqs": "0.0.5",
"parseuri": "0.0.5",
"socket.io-parser": "~3.2.0",
"to-array": "0.1.4"
},
"dependencies": {
"component-emitter": {
"version": "1.2.1",
@ -10762,9 +10693,53 @@
"requires": {
"ms": "2.0.0"
}
},
"engine.io-client": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-3.2.1.tgz",
"integrity": "sha512-y5AbkytWeM4jQr7m/koQLc5AxpRKC1hEVUb/s1FUAWEJq5AzJJ4NLvzuKPuxtDi5Mq755WuDvZ6Iv2rXj4PTzw==",
"requires": {
"component-emitter": "1.2.1",
"component-inherit": "0.0.3",
"debug": "~3.1.0",
"engine.io-parser": "~2.1.1",
"has-cors": "1.1.0",
"indexof": "0.0.1",
"parseqs": "0.0.5",
"parseuri": "0.0.5",
"ws": "~3.3.1",
"xmlhttprequest-ssl": "~1.5.4",
"yeast": "0.1.2"
}
},
"socket.io-client": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-2.1.1.tgz",
"integrity": "sha512-jxnFyhAuFxYfjqIgduQlhzqTcOEQSn+OHKVfAxWaNWa7ecP7xSNk2Dx/3UEsDcY7NcFafxvNvKPmmO7HTwTxGQ==",
"requires": {
"backo2": "1.0.2",
"base64-arraybuffer": "0.1.5",
"component-bind": "1.0.0",
"component-emitter": "1.2.1",
"debug": "~3.1.0",
"engine.io-client": "~3.2.0",
"has-binary2": "~1.0.2",
"has-cors": "1.1.0",
"indexof": "0.0.1",
"object-component": "0.0.3",
"parseqs": "0.0.5",
"parseuri": "0.0.5",
"socket.io-parser": "~3.2.0",
"to-array": "0.1.4"
}
}
}
},
"socket.io-adapter": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-1.1.1.tgz",
"integrity": "sha1-KoBeihTWNyEk3ZFZrUUC+MsH8Gs="
},
"socket.io-parser": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.2.0.tgz",
@ -10795,6 +10770,11 @@
}
}
},
"sockette": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/sockette/-/sockette-2.0.6.tgz",
"integrity": "sha512-W6iG8RGV6Zife3Cj+FhuyHV447E6fqFM2hKmnaQrTvg3OydINV3Msj3WPFbX76blUlUxvQSMMMdrJxce8NqI5Q=="
},
"sort-keys": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz",

View file

@ -86,6 +86,7 @@
"resolve-url-loader": "^3.1.1",
"sass-loader": "^7.3.1",
"sinon": "^7.5.0",
"sockette": "^2.0.6",
"style-loader": "^0.23.1",
"sugarss": "^2.0.0",
"svg-url-loader": "^2.3.3",

View file

@ -4,6 +4,7 @@ import Router from "vue-router";
import PhotoPrism from "photoprism.vue";
import Routes from "routes";
import Api from "common/api";
import Socket from "common/websocket";
import Config from "common/config";
import Clipboard from "common/clipboard";
import Components from "component/components";
@ -33,6 +34,7 @@ Vue.prototype.$alert = Alert;
Vue.prototype.$viewer = viewer;
Vue.prototype.$session = Session;
Vue.prototype.$api = Api;
Vue.prototype.$socket = Socket;
Vue.prototype.$config = config;
Vue.prototype.$clipboard = clipboard;

View file

@ -2,17 +2,17 @@ import Event from "pubsub-js";
const Alert = {
info: function (message) {
Event.publish("alert.info", message);
Event.publish("alert.info", {msg: message});
},
warning: function (message) {
Event.publish("alert.warning", message);
Event.publish("alert.warning", {msg: message});
},
error: function (message) {
Event.publish("alert.error", message);
Event.publish("alert.error", {msg: message});
},
success: function (message) {
Event.publish("alert.success", message);
Event.publish("alert.success", {msg: message});
},
};
export default Alert;
export default Alert;

View file

@ -1,6 +1,7 @@
import "@babel/polyfill/noConflict";
import axios from "axios";
import Event from "pubsub-js";
import "@babel/polyfill/noConflict";
import Alert from "common/alert";
const Api = axios.create({
baseURL: "/api/v1",
@ -36,7 +37,7 @@ Api.interceptors.response.use(function (response) {
}
Event.publish("ajax.end");
Event.publish("alert.error", errorMessage);
Alert.error(errorMessage);
return Promise.reject(error);
});

View file

@ -0,0 +1,22 @@
import Sockette from "sockette";
import Event from "pubsub-js";
const host = window.location.host;
const Socket = new Sockette("ws://" + host + "/api/v1/ws", {
timeout: 5e3,
onopen: e => {
console.log('Connected!', e);
Socket.send("hello world");
},
onmessage: e => {
const m = JSON.parse(e.data);
console.log('Received:', m);
Event.publish(m.event, m.data);
},
onreconnect: e => console.log('Reconnecting...', e),
onmaximum: e => console.log('Stop Attempting!', e),
onclose: e => console.log('Closed!', e),
onerror: e => console.log('Error:', e)
});
export default Socket;

View file

@ -43,24 +43,30 @@
Event.unsubscribe(this.subscriptionId);
},
methods: {
handleAlertEvent: function (ev, message) {
handleAlertEvent: function (ev, data) {
const type = ev.split('.')[1];
// get message from data object
let m = data.msg;
// first letter uppercase
m = m.replace(/^./, m[0].toUpperCase());
switch (type) {
case 'warning':
this.addWarningMessage(message);
this.addWarningMessage(m);
break;
case 'error':
this.addErrorMessage(message);
this.addErrorMessage(m);
break;
case 'success':
this.addSuccessMessage(message);
this.addSuccessMessage(m);
break;
case 'info':
this.addInfoMessage(message);
this.addInfoMessage(m);
break;
default:
alert(message);
alert(m);
}
},

View file

@ -131,7 +131,7 @@
Api.post("batch/photos/private", {"ids": this.selection}).then(function () {
Event.publish("ajax.end");
Event.publish("alert.success", "Toggled private flag");
this.$alert.success("Toggled private flag");
ctx.clearClipboard();
ctx.refresh();
}).catch(() => {
@ -145,7 +145,7 @@
Api.post("batch/photos/story", {"ids": this.selection}).then(function () {
Event.publish("ajax.end");
Event.publish("alert.success", "Toggled story flag");
this.$alert.success("Toggled story flag");
ctx.clearClipboard();
ctx.refresh();
}).catch(() => {
@ -161,7 +161,7 @@
Api.post("batch/photos/delete", {"ids": this.selection}).then(function () {
Event.publish("ajax.end");
Event.publish("alert.success", "Photos deleted");
this.$alert.success("Photos deleted");
ctx.clearClipboard();
ctx.refresh();
}).catch(() => {

View file

@ -3,7 +3,8 @@
<v-form ref="form" class="p-photo-import" lazy-validation @submit.prevent="submit" dense>
<v-container fluid>
<p class="subheading">
<span v-if="busy">Importing files from directory...</span>
<span v-if="fileName">Indexed {{ fileName}}...</span>
<span v-else-if="busy">Importing files from directory...</span>
<span v-else-if="completed">Done.</span>
<span v-else>Press button to import photos from directory...</span>
</p>
@ -26,7 +27,6 @@
</template>
<script>
import Api from "common/api";
import Event from "pubsub-js";
export default {
@ -36,6 +36,8 @@
started: false,
busy: false,
completed: 0,
subscriptionId: '',
fileName: '',
}
},
methods: {
@ -46,21 +48,45 @@
this.started = Date.now();
this.busy = true;
this.completed = 0;
this.$alert.info("Importing photos...");
this.fileName = '';
const ctx = this;
Api.post('import').then(function () {
Event.publish("alert.success", "Import complete");
this.$api.post('import').then(function () {
ctx.busy = false;
ctx.completed = 100;
this.fileName = '';
}).catch(function () {
Event.publish("alert.error", "Import failed");
this.$alert.error("Import failed");
ctx.busy = false;
ctx.completed = 0;
this.fileName = '';
});
},
}
handleEvent(ev, data) {
const type = ev.split('.')[1];
switch (type) {
case 'file':
this.busy = true;
this.completed = 0;
this.fileName = data.fileName;
break;
case 'completed':
this.busy = false;
this.completed = 100;
this.fileName = '';
break;
default:
console.log(data)
}
},
},
created() {
this.subscriptionId = Event.subscribe('import', this.handleEvent);
},
destroyed() {
Event.unsubscribe(this.subscriptionId);
},
};
</script>

View file

@ -3,7 +3,8 @@
<v-form ref="form" class="p-photo-index" lazy-validation @submit.prevent="submit" dense>
<v-container fluid>
<p class="subheading">
<span v-if="busy">Re-indexing existing files and photos...</span>
<span v-if="fileName">Indexed {{ fileName}}...</span>
<span v-else-if="busy">Re-indexing existing files and photos...</span>
<span v-else-if="completed">Done.</span>
<span v-else>Press button to re-index existing files and photos...</span>
</p>
@ -26,7 +27,6 @@
</template>
<script>
import Api from "common/api";
import Event from "pubsub-js";
export default {
@ -36,6 +36,8 @@
started: false,
busy: false,
completed: 0,
subscriptionId: '',
fileName: '',
}
},
methods: {
@ -46,21 +48,45 @@
this.started = Date.now();
this.busy = true;
this.completed = 0;
this.$alert.info("Indexing photos...");
this.fileName = '';
const ctx = this;
Api.post('index').then(function () {
Event.publish("alert.success", "Indexing complete");
this.$api.post('index').then(function () {
ctx.busy = false;
ctx.completed = 100;
this.fileName = '';
}).catch(function () {
Event.publish("alert.error", "Indexing failed");
this.$alert.error("Indexing failed");
ctx.busy = false;
ctx.completed = 0;
this.fileName = '';
});
},
}
handleEvent(ev, data) {
const type = ev.split('.')[1];
switch (type) {
case 'file':
this.busy = true;
this.completed = 0;
this.fileName = data.fileName;
break;
case 'completed':
this.busy = false;
this.completed = 100;
this.fileName = '';
break;
default:
console.log(data)
}
},
},
created() {
this.subscriptionId = Event.subscribe('index', this.handleEvent);
},
destroyed() {
Event.unsubscribe(this.subscriptionId);
},
};
</script>

View file

@ -29,7 +29,6 @@
</template>
<script>
import Api from "common/api";
import Event from "pubsub-js";
export default {
@ -80,7 +79,7 @@
formData.append('files', file);
await Api.post('upload/' + ctx.started,
await this.$api.post('upload/' + ctx.started,
formData,
{
headers: {
@ -90,7 +89,7 @@
).then(function () {
ctx.completed = Math.round((ctx.current / ctx.total) * 100);
}).catch(function () {
Event.publish("alert.error", "Upload failed");
this.$alert.error("Upload failed");
});
}
}
@ -99,12 +98,12 @@
this.indexing = true;
const ctx = this;
Api.post('import/upload/' + this.started).then(function () {
Event.publish("alert.success", "Upload complete");
this.$api.post('import/upload/' + this.started).then(function () {
this.$alert.success("Upload complete");
ctx.busy = false;
ctx.indexing = false;
}).catch(function () {
Event.publish("alert.error", "Failure while importing uploaded files");
this.$alert.error("Failure while importing uploaded files");
ctx.busy = false;
ctx.indexing = false;
});

10
go.mod
View file

@ -13,12 +13,14 @@ require (
github.com/dsoprea/go-exif v0.0.0-20190901173045-3ce78807c90f
github.com/dsoprea/go-logging v0.0.0-20190409182557-13b4fff49234 // indirect
github.com/dustin/go-humanize v1.0.0 // indirect
github.com/gin-contrib/sse v0.0.0-20170109093832-22d885f9ecc7 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/gin-gonic/gin v1.3.0
github.com/go-errors/errors v1.0.1 // indirect
github.com/golang/geo v0.0.0-20190507233405-a0e886e97a51 // indirect
github.com/golang/protobuf v1.3.2 // indirect
github.com/golang/snappy v0.0.1 // indirect
github.com/gorilla/websocket v1.4.0 // indirect
github.com/google/go-cmp v0.3.1 // indirect
github.com/gorilla/websocket v1.4.1
github.com/gosimple/slug v1.5.0
github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4 // indirect
github.com/grpc-ecosystem/grpc-gateway v1.5.1 // indirect
@ -26,6 +28,7 @@ require (
github.com/json-iterator/go v1.1.5 // indirect
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 // indirect
github.com/kr/pretty v0.1.0 // indirect
github.com/leandro-lugaresi/hub v1.1.0
github.com/lucasb-eyer/go-colorful v1.0.2
github.com/mattn/go-isatty v0.0.4 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
@ -60,11 +63,12 @@ require (
github.com/ugorji/go v1.1.7 // indirect
github.com/unrolled/render v0.0.0-20181210145518-4c664cb3ad2f // indirect
github.com/urfave/cli v1.20.0
go.uber.org/atomic v1.4.0 // indirect
golang.org/x/crypto v0.0.0-20190424203555-c05e17bb3b2d // indirect
golang.org/x/image v0.0.0-20181116024801-cd38e8056d9b // indirect
golang.org/x/sync v0.0.0-20190423024810-112230192c58 // indirect
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a // indirect
golang.org/x/text v0.3.1 // indirect
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 // indirect
gopkg.in/go-playground/assert.v1 v1.2.1 // indirect
gopkg.in/go-playground/validator.v8 v8.18.2 // indirect
gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce // indirect

20
go.sum
View file

@ -79,8 +79,8 @@ github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/gin-contrib/sse v0.0.0-20170109093832-22d885f9ecc7 h1:AzN37oI0cOS+cougNAV9szl6CVoj2RYwzS3DpUQNtlY=
github.com/gin-contrib/sse v0.0.0-20170109093832-22d885f9ecc7/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.3.0 h1:kCmZyPklC0gVdL728E6Aj20uYBJV93nj/TkwBTKhFbs=
github.com/gin-gonic/gin v1.3.0/go.mod h1:7cKuhb5qV2ggCFctp2fJQ+ErvciLZrIeoOSOm6mUr7Y=
github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w=
@ -106,6 +106,8 @@ github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfb
github.com/golang/protobuf v1.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db h1:woRePGFeVFfLKN/pOkfl+p/TAqKOfFu+7KPlMVpok/w=
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4=
@ -115,6 +117,8 @@ github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c h1:964Od4U6p2jUkFxvCy
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/shlex v0.0.0-20181106134648-c34317bd91bf/go.mod h1:RpwtwJQFrIEPstU94h88MWPXP2ektJZ8cZ0YntAmXiE=
@ -125,8 +129,8 @@ github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51
github.com/gorilla/mux v0.0.0-20170228224354-599cba5e7b61/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/mux v1.6.2 h1:Pgr17XVTNXAk3q/r4CpKzC5xBM/qW1uVLV+IhRZpIIk=
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q=
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM=
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gosimple/slug v1.5.0 h1:AIIjgCjHcLpX8LzM2NpG4QGW9kUfqv0OLiFRfPv/H3E=
github.com/gosimple/slug v1.5.0/go.mod h1:ER78kgg1Mv0NQGlXiDe57DpCyfbNywXXZ9mIorhxAf0=
github.com/grpc-ecosystem/go-grpc-middleware v0.0.0-20171020063731-82921fcf811d/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
@ -163,6 +167,8 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/leandro-lugaresi/hub v1.1.0 h1:yHYA0WsMYaJd+I6J24nYlCP2CFD4RTnhaHCRmKjv3q4=
github.com/leandro-lugaresi/hub v1.1.0/go.mod h1:IVKrfZTYfU1SbWCGQMHNGYdW4j1Pl7Cg8gr6sSeT/84=
github.com/lib/pq v1.1.0 h1:/5u4a+KGJptBRqGzPvYQL9p0d/tPR4S31+Tnzj9lEO4=
github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lucasb-eyer/go-colorful v1.0.2 h1:mCMFu6PgSozg9tDNMMK3g18oJBX7oYGrC09mS6CXfO4=
@ -313,6 +319,8 @@ github.com/yookoala/realpath v1.0.0/go.mod h1:gJJMA9wuX7AcqLy1+ffPatSCySA1FQ2S8Y
go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
go.uber.org/atomic v1.3.2 h1:2Oa65PReHzfn29GpvgsYwloV9AVFHPDk8tYxt2c2tr4=
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.4.0 h1:cxzIVoETapQEqDhQu3QfnvXAV4AlzcvUCxkVUFw3+EU=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/multierr v1.1.0 h1:HoEmRHQPVSqub6w2z2d2EOVs2fjyFRGyofhKuyDq0QI=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/zap v1.9.1 h1:XCJQEf3W6eZaVwhRBof6ImoYGJSITeKWsyeh3HFu/5o=
@ -347,8 +355,6 @@ golang.org/x/sync v0.0.0-20181108010431-42b317875d0f h1:Bl/8QSvNqXvPGPGXa2z5xUTm
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@ -366,6 +372,8 @@ golang.org/x/text v0.3.1/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c h1:fqgJT0MGcGpPgpWU7VRdRjuArfcOvC4AoJmILihzhDg=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20181105230042-78dc5bac0cac h1:0Nb35Izc6T6Yz1iGmRc4cg14cxRaFjbjD4hWFI6JNJ8=

View file

@ -3,9 +3,11 @@ package api
import (
"fmt"
"net/http"
"path/filepath"
"time"
"github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/event"
"github.com/gin-gonic/gin"
"github.com/photoprism/photoprism/internal/photoprism"
@ -46,14 +48,17 @@ func Import(router *gin.RouterGroup, conf *config.Config) {
path = path + subPath
}
log.Infof("importing photos from %s", path)
event.Info(fmt.Sprintf("importing photos from \"%s\"", filepath.Base(path)))
initImporter(conf)
importer.ImportPhotosFromDirectory(path)
elapsed := time.Since(start)
elapsed := int(time.Since(start).Seconds())
c.JSON(http.StatusOK, gin.H{"message": fmt.Sprintf("completed import in %s", elapsed)})
event.Success(fmt.Sprintf("completed import in %d s", elapsed))
event.Publish("import.completed", event.Data{"path": path, "seconds": elapsed})
c.JSON(http.StatusOK, gin.H{"message": fmt.Sprintf("completed import in %d s", elapsed)})
})
}

View file

@ -3,10 +3,12 @@ package api
import (
"fmt"
"net/http"
"path/filepath"
"time"
"github.com/gin-gonic/gin"
"github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/event"
"github.com/photoprism/photoprism/internal/photoprism"
)
@ -33,14 +35,17 @@ func Index(router *gin.RouterGroup, conf *config.Config) {
start := time.Now()
path := conf.OriginalsPath()
log.Infof("indexing photos in %s", path)
event.Info(fmt.Sprintf("indexing photos in \"%s\"", filepath.Base(path)))
initIndexer(conf)
indexer.IndexAll()
elapsed := time.Since(start)
elapsed := int(time.Since(start).Seconds())
c.JSON(http.StatusOK, gin.H{"message": fmt.Sprintf("indexing completed in %s", elapsed)})
event.Success(fmt.Sprintf("indexing completed in %d s", elapsed))
event.Publish("index.completed", event.Data{"path": path, "seconds": elapsed})
c.JSON(http.StatusOK, gin.H{"message": fmt.Sprintf("indexing completed in %d s", elapsed)})
})
}

79
internal/api/websocket.go Normal file
View file

@ -0,0 +1,79 @@
package api
import (
"time"
"github.com/gin-gonic/gin"
"github.com/gorilla/websocket"
"github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/event"
)
var wsConnection = websocket.Upgrader{}
var wsTimeout = 60 * time.Second
func wsReader(ws *websocket.Conn) {
defer ws.Close()
ws.SetReadLimit(512)
ws.SetReadDeadline(time.Now().Add(wsTimeout))
ws.SetPongHandler(func(string) error { ws.SetReadDeadline(time.Now().Add(wsTimeout)); return nil })
for {
_, m, err := ws.ReadMessage()
if err != nil {
break
}
log.Infof("received: %s", m)
}
}
func wsWriter(ws *websocket.Conn, conf *config.Config) {
pingTicker := time.NewTicker(10 * time.Second)
s := event.Subscribe("index.*", "alert.*")
defer func() {
pingTicker.Stop()
event.Unsubscribe(s)
ws.Close()
}()
for {
select {
case <-pingTicker.C:
ws.SetWriteDeadline(time.Now().Add(10 * time.Second))
if err := ws.WriteMessage(websocket.PingMessage, []byte{}); err != nil {
return
}
case msg := <-s.Receiver:
ws.SetWriteDeadline(time.Now().Add(10 * time.Second))
if err := ws.WriteJSON(gin.H{"event": msg.Name, "data": msg.Fields}); err != nil {
log.Errorf("write json: %s", err)
return
}
}
}
}
// GET /api/v1/ws
func Websocket(router *gin.RouterGroup, conf *config.Config) {
router.GET("/ws", func(c *gin.Context) {
w := c.Writer
r := c.Request
ws, err := wsConnection.Upgrade(w, r, nil)
if err != nil {
log.Errorf("upgrade error: %s", err)
return
}
defer ws.Close()
log.Infof("websocket connected: %s", c.Request.RemoteAddr)
go wsWriter(ws, conf)
wsReader(ws)
})
}

61
internal/event/hub.go Normal file
View file

@ -0,0 +1,61 @@
package event
import (
"github.com/leandro-lugaresi/hub"
"github.com/sirupsen/logrus"
)
type Hub = hub.Hub
type Data = hub.Fields
type Message = hub.Message
var log *logrus.Logger
var channelCap = 10
var sharedHub = NewHub()
func init() {
log = logrus.StandardLogger()
}
func NewHub () *Hub {
return hub.New()
}
func SharedHub() *Hub {
return sharedHub
}
func Error(msg string) {
log.Error(msg)
Publish("alert.error", Data{"msg": msg})
}
func Success(msg string) {
log.Info(msg)
Publish("alert.success", Data{"msg": msg})
}
func Info(msg string) {
log.Info(msg)
Publish("alert.info", Data{"msg": msg})
}
func Warning(msg string) {
log.Warn(msg)
Publish("alert.warning", Data{"msg": msg})
}
func Publish (event string, data Data) {
log.Infof("publish %s: %v", event, data)
SharedHub().Publish(Message{
Name: event,
Fields: data,
})
}
func Subscribe(topics ...string) hub.Subscription {
return SharedHub().Subscribe(channelCap, topics...)
}
func Unsubscribe(s hub.Subscription) {
SharedHub().Unsubscribe(s)
}

View file

@ -0,0 +1,31 @@
package event
import (
"testing"
"github.com/leandro-lugaresi/hub"
"github.com/stretchr/testify/assert"
)
func TestSharedHub(t *testing.T) {
h := SharedHub()
assert.IsType(t, &hub.Hub{}, h)
}
func TestPublishSubscribe(t *testing.T) {
s := Subscribe("foo.bar")
assert.IsType(t, hub.Subscription{}, s)
Publish("foo.bar", Data{"id": 13})
msg := <-s.Receiver
t.Logf("receive msg with topic %s: %v\n", msg.Name, msg.Fields)
assert.Equal(t, "foo.bar", msg.Name)
assert.Equal(t, Data{"id": 13}, msg.Fields)
Unsubscribe(s)
}

View file

@ -9,6 +9,7 @@ import (
"strings"
"github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/event"
"github.com/photoprism/photoprism/internal/models"
"github.com/photoprism/photoprism/internal/util"
@ -78,11 +79,15 @@ func (i *Importer) ImportPhotosFromDirectory(importPath string) {
relatedFiles, mainFile, err := mediaFile.RelatedFiles()
if err != nil {
log.Errorf("could not import \"%s\": %s", mediaFile.RelativeFilename(importPath), err.Error())
event.Error(fmt.Sprintf("could not import \"%s\": %s", mediaFile.RelativeFilename(importPath), err.Error()))
return nil
}
event.Publish("import.file", event.Data{
"fileName": mainFile.Filename(),
})
for _, relatedMediaFile := range relatedFiles {
relativeFilename := relatedMediaFile.RelativeFilename(importPath)

View file

@ -10,6 +10,7 @@ import (
"github.com/jinzhu/gorm"
"github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/event"
"github.com/photoprism/photoprism/internal/models"
"github.com/photoprism/photoprism/internal/util"
)
@ -338,6 +339,17 @@ func (i *Indexer) indexMediaFile(mediaFile *MediaFile) string {
file.FilePortrait = mediaFile.Width() < mediaFile.Height()
}
event.Publish("index.file", event.Data{
"photoID": file.PhotoID,
"filePrimary": file.FilePrimary,
"fileMissing": file.FileMissing,
"fileName": file.FileName,
"fileHash": file.FileHash,
"fileType": file.FileType,
"fileMime": file.FileMime,
"updated": fileQuery.Error == nil,
})
if fileQuery.Error == nil {
i.db.Unscoped().Save(&file)
return indexResultUpdated

View file

@ -15,6 +15,12 @@ func registerRoutes(router *gin.Engine, conf *config.Config) {
// Static assets like js and css files
router.Static("/static", conf.HttpStaticPath())
// socket.io
/* s := router.Group("/socket.io")
{
api.Socket(s, conf)
} */
// JSON-REST API Version 1
v1 := router.Group("/api/v1")
{
@ -49,6 +55,8 @@ func registerRoutes(router *gin.Engine, conf *config.Config) {
api.GetSettings(v1, conf)
api.SaveSettings(v1, conf)
api.Websocket(v1, conf)
}
// Default HTML page (client-side routing implemented via Vue.js)