@live-change/image-service 0.2.29 → 0.2.34

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,24 @@
1
+ const definition = require('./definition.js')
2
+
3
+ const config = definition.config
4
+ const uploadConfig = config.upload || {}
5
+
6
+ const {
7
+ maxUploadSize = 10 * 1024 * 1024,
8
+ maxUploadWidth = 2048,
9
+ maxUploadHeight = 2048,
10
+ maxProcessableSize = 50 * 1024 * 1025,
11
+ maxProcessableWidth = 10000,
12
+ maxProcessableHeight = 10000,
13
+ maxProcessablePixels = 6000*6000
14
+ } = uploadConfig
15
+
16
+ definition.clientConfig = {
17
+ maxUploadSize,
18
+ maxUploadWidth,
19
+ maxUploadHeight,
20
+ maxProcessableSize,
21
+ maxProcessableWidth,
22
+ maxProcessableHeight,
23
+ maxProcessablePixels
24
+ }
package/endpoint.js CHANGED
@@ -1,8 +1,151 @@
1
1
  const app = require("@live-change/framework").app()
2
2
  const definition = require('./definition.js')
3
3
 
4
- const storageDir = `./storage/images/`
5
- const uploadsDir = `./uploads/`
4
+ const { Image } = require('./image.js')
5
+
6
+ const fs = require("fs")
7
+ const path = require("path")
8
+ const sharp = require('sharp')
9
+
10
+ const config = definition.config
11
+
12
+ const imagesPath = config.imagesPath || `./storage/images/`
13
+
14
+ function normalizeFormat(f1) {
15
+ f1 = f1.toLowerCase().trim()
16
+ if(f1 == 'jpg') f1 = 'jpeg'
17
+ return f1
18
+ }
19
+
20
+ function isFormatsIdentical(f1, f2) {
21
+ return normalizeFormat(f1) == normalizeFormat(f2)
22
+ }
23
+
24
+ function fileExists(fn) {
25
+ return new Promise((resolve, reject) => {
26
+ fs.access(fn, fs.F_OK, (err) => {
27
+ resolve(!err)
28
+ })
29
+ })
30
+ }
31
+
32
+ function delay(ms) {
33
+ return new Promise((resolve, reject) => setTimeout(resolve, ms))
34
+ }
35
+ async function getImageMetadata(picture, version) {
36
+ for(let t = 0; t < 5; t++) {
37
+ const metadata = await Image.get(picture)
38
+ console.log("MD", metadata)
39
+ if(metadata.original && metadata[version]) return metadata
40
+ await delay(200 * Math.pow(2, t))
41
+ }
42
+ return null
43
+ }
44
+
45
+ function sanitizeImageId(id) {
46
+ return id.replace(/[^a-zA-Z0-9\[\]@\.-]/g,"_")
47
+ }
48
+
49
+ async function handleImageGet(req, res, params) {
50
+ const { picture, version } = params
51
+ const metadata = await getImageMetadata(picture, version)
52
+ console.log("PIC METADATA", picture, version, "=>", metadata)
53
+ if(!metadata) {
54
+ res.status(404)
55
+ res.send("Picture not found")
56
+ return
57
+ }
58
+ const imagePrefix = imagesPath + sanitizeImageId(picture) + '/' + version.replace(/[^a-zA-Z0-9-]/g,"_")
59
+ const sourceFilePath = path.resolve(imagePrefix + '.' + metadata.original.extension)
60
+ console.log("SOURCE PIC PATH", sourceFilePath)
61
+ if(!(await fileExists(sourceFilePath))) {
62
+ res.status(404)
63
+ console.error("PICTURE FILE NOT FOUND", sourceFilePath)
64
+ res.send("Picture file not found")
65
+ return
66
+ }
67
+
68
+ const kernel = "lanczos3"
69
+
70
+ switch(params.type) {
71
+ case "auto": {
72
+ if(params.format && !isFormatsIdentical(params.format, metadata.original.extension)) {
73
+ console.log("CONVERTING PICTURE!", metadata.original.extension, params.format)
74
+ const normalized = normalizeFormat(params.format)
75
+ const convertedFilePath = path.resolve(imagePrefix + '.' + normalized)
76
+ if(!(await fileExists(convertedFilePath))) {
77
+ await sharp(sourceFilePath).toFile(convertedFilePath)
78
+ }
79
+ res.sendFile(convertedFilePath)
80
+ } else res.sendFile(sourceFilePath)
81
+ } break;
82
+ case "width": {
83
+ const width = +params.width
84
+ if(!(width > 0)) {
85
+ res.status(400)
86
+ res.send("Bad parameter value")
87
+ return
88
+ }
89
+ //if(width > metadata.original.width) return res.sendFile(sourceFilePath)
90
+ const normalized = normalizeFormat(params.format || metadata.original.extension)
91
+ const convertedFilePath = path.resolve(imagePrefix + `-width-${width}.${normalized}`)
92
+ if(!(await fileExists(convertedFilePath))) {
93
+ await sharp(sourceFilePath)
94
+ .resize({
95
+ width,
96
+ kernel
97
+ })
98
+ .toFile(convertedFilePath)
99
+ }
100
+ res.sendFile(convertedFilePath)
101
+ } break;
102
+ case "height": {
103
+ const height = +params.height
104
+ if(!(height > 0)) {
105
+ res.status(400)
106
+ res.send("Bad parameter value")
107
+ return
108
+ }
109
+ //if(height > metadata.original.height) return res.sendFile(sourceFilePath)
110
+ const normalized = normalizeFormat(params.format || metadata.original.extension)
111
+ const convertedFilePath = path.resolve(imagePrefix + `-height-${height}.${normalized}`)
112
+ if(!(await fileExists(convertedFilePath))) {
113
+ await sharp(sourceFilePath)
114
+ .resize({
115
+ height,
116
+ kernel
117
+ })
118
+ .toFile(convertedFilePath)
119
+ }
120
+ res.sendFile(convertedFilePath)
121
+ } break;
122
+ case "rect": {
123
+ const width = +params.width
124
+ const height = +params.height
125
+ if(!(height > 0 && width>0)) {
126
+ res.status(400)
127
+ res.send("Bad parameter value")
128
+ return
129
+ }
130
+ //if(height > metadata.original.height) return res.sendFile(sourceFilePath)
131
+ //if(width > metadata.original.width) return res.sendFile(sourceFilePath)
132
+ const normalized = normalizeFormat(params.format || metadata.original.extension)
133
+ const convertedFilePath = path.resolve(imagePrefix + `-rect-${width}-${height}.${normalized}`)
134
+ if(!(await fileExists(convertedFilePath))) {
135
+ await sharp(sourceFilePath)
136
+ .resize({
137
+ width, height,
138
+ fit: sharp.fit.cover,
139
+ position: sharp.strategy.attention,
140
+ kernel
141
+ })
142
+ .toFile(convertedFilePath)
143
+ }
144
+ res.sendFile(convertedFilePath)
145
+ }
146
+ }
147
+ }
148
+
6
149
 
7
150
  const express = require("express")
8
151
 
@@ -10,39 +153,30 @@ definition.endpoint({
10
153
  name: 'image',
11
154
  create() {
12
155
  const expressApp = express()
13
- expressApp.get('/:image/:version/width-:width.:format', (req, res) => handleImageGet(req, res, {
14
- ...req.params,
15
- type: "width"
16
- }))
17
- expressApp.get('/:image/:version/width-:width', (req, res) => handleImageGet(req, res, {
18
- ...req.params,
19
- type: "width"
20
- }))
21
- expressApp.get('/:image/:version/height-:height.:format', (req, res) => handleImageGet(req, res, {
22
- ...req.params,
23
- type: "height"
24
- }))
25
- expressApp.get('/:image/:version/height-:height', (req, res) => handleImageGet(req, res, {
26
- ...req.params,
27
- type: "height"
28
- }))
29
- expressApp.get('/:image/:version/rect-:width-:height.:format', (req, res) => handleImageGet(req, res, {
30
- ...req.params,
31
- type: "rect"
32
- }))
33
- expressApp.get('/:image/:version/rect-:width-:height', (req, res) => handleImageGet(req, res, {
34
- ...req.params,
35
- type: "rect"
36
- }))
37
- expressApp.get('/:image/:version.:format', (req, res) => handleImageGet(req, res, {
38
- ...req.params,
39
- type: "auto"
40
- }))
41
- expressApp.get('/:image/:version', (req, res) => handleImageGet(req, res, {
42
- ...req.params,
43
- type: "auto"
44
- }))
45
-
156
+ expressApp.get('/:image/:version/width-:width.:format',
157
+ (req, res) => handleImageGet(req, res, { ...req.params, type: "width" })
158
+ )
159
+ expressApp.get('/:image/:version/width-:width',
160
+ (req, res) => handleImageGet(req, res, { ...req.params, type: "width" })
161
+ )
162
+ expressApp.get('/:image/:version/height-:height.:format',
163
+ (req, res) => handleImageGet(req, res, { ...req.params, type: "height" })
164
+ )
165
+ expressApp.get('/:image/:version/height-:height',
166
+ (req, res) => handleImageGet(req, res, { ...req.params, type: "height" })
167
+ )
168
+ expressApp.get('/:image/:version/rect-:width-:height.:format',
169
+ (req, res) => handleImageGet(req, res, { ...req.params, type: "rect" })
170
+ )
171
+ expressApp.get('/:image/:version/rect-:width-:height',
172
+ (req, res) => handleImageGet(req, res, { ...req.params, type: "rect" })
173
+ )
174
+ expressApp.get('/:image/:version.:format',
175
+ (req, res) => handleImageGet(req, res, { ...req.params, type: "auto" })
176
+ )
177
+ expressApp.get('/:image/:version',
178
+ (req, res) => handleImageGet(req, res, { ...req.params, type: "auto" })
179
+ )
46
180
  return expressApp
47
181
  }
48
182
  })
package/fsUtils.js CHANGED
@@ -1,3 +1,4 @@
1
+ const fs = require('fs')
1
2
 
2
3
  function move(from, to) {
3
4
  return new Promise((resolve,reject) => {
package/image.js CHANGED
@@ -1,8 +1,9 @@
1
1
  const app = require("@live-change/framework").app()
2
2
  const definition = require('./definition.js')
3
+ const config = definition.config
3
4
 
4
- const storageDir = `./storage/images/`
5
- const uploadsDir = `./uploads/`
5
+ const imagesPath = config.imagesPath || `./storage/images/`
6
+ const uploadsPath = config.uploadsPath || `./storage/uploads/`
6
7
 
7
8
  const Image = definition.model({
8
9
  name: "Image",
@@ -48,6 +49,9 @@ const download = require('download')
48
49
  definition.action({
49
50
  name: "createEmptyImage",
50
51
  properties: {
52
+ image: {
53
+ type: Image
54
+ },
51
55
  name: {
52
56
  type: String,
53
57
  validation: ['nonEmpty']
@@ -66,10 +70,16 @@ definition.action({
66
70
  }
67
71
  },
68
72
  /// TODO: accessControl
69
- async execute({ name, purpose, owner, ownerType }, { client, service }, emit) {
70
- const image = app.generateUid()
73
+ async execute({ image, name, purpose, owner, ownerType }, { client, service }, emit) {
74
+ if(!image) {
75
+ image = app.generateUid()
76
+ } else {
77
+ // TODO: check id source session
78
+ const existing = await Image.get(image)
79
+ if (existing) throw 'already_exists'
80
+ }
71
81
 
72
- const dir = `${storageDir}${image}`
82
+ const dir = `${imagesPath}${image}`
73
83
 
74
84
  emit({
75
85
  type: "ownerOwnedImageCreated",
@@ -93,6 +103,8 @@ definition.action({
93
103
  }
94
104
  })
95
105
 
106
+ const Upload = definition.foreignModel('upload', 'Upload')
107
+
96
108
  definition.action({
97
109
  name: "uploadImage",
98
110
  properties: {
@@ -104,26 +116,26 @@ definition.action({
104
116
  properties: {
105
117
  width: { type: Number },
106
118
  height: { type: Number },
107
- uploadId: { type: String }
119
+ upload: { type: Upload }
108
120
  }
109
121
  }
110
122
  },
111
123
  /// TODO: accessControl!
112
124
  waitForEvents: true,
113
125
  async execute({ image, original }, { client, service }, emit) {
114
- const upload = await app.dao.get(['database', 'tableObject', app.databaseName, 'uploads', original.uploadId])
115
- if(!upload) throw new Error("upload_not_found")
116
- if(upload.state!='done') throw new Error("upload_not_done")
126
+ const uploadRow = await Upload.get(original.upload)
127
+ if(!uploadRow) throw new Error("upload_not_found")
128
+ if(uploadRow.state!='done') throw new Error("upload_not_done")
117
129
 
118
- let extension = upload.fileName.match(/\.([A-Z0-9]+)$/i)[1].toLowerCase()
130
+ let extension = uploadRow.fileName.match(/\.([A-Z0-9]+)$/i)[1].toLowerCase()
119
131
  if(extension == 'jpg') extension = "jpeg"
120
- const dir = `${storageDir}${image}`
132
+ const dir = `${imagesPath}${image}`
121
133
 
122
134
  emit({
123
135
  type: "ownerOwnedImageUpdated",
124
136
  image,
125
137
  data: {
126
- fileName: upload.fileName,
138
+ fileName: uploadRow.fileName,
127
139
  original: {
128
140
  width: original.width,
129
141
  height: original.height,
@@ -133,8 +145,11 @@ definition.action({
133
145
  }
134
146
  })
135
147
 
136
- await move(`${uploadsDir}${upload.id}`, `${dir}/original.${extension}`)
137
- await app.dao.request(['database', 'delete'], app.databaseName, 'uploads', upload.id)
148
+ await move(`${uploadsPath}${uploadRow.id}`, `${dir}/original.${extension}`)
149
+ await app.trigger({
150
+ type: 'uploadUsed',
151
+ upload: uploadRow.id
152
+ })
138
153
 
139
154
  return image
140
155
  }
@@ -143,6 +158,9 @@ definition.action({
143
158
  definition.action({
144
159
  name: "createImage",
145
160
  properties: {
161
+ image: {
162
+ type: Image
163
+ },
146
164
  name: {
147
165
  type: String,
148
166
  validation: ['nonEmpty']
@@ -152,7 +170,7 @@ definition.action({
152
170
  properties: {
153
171
  width: { type: Number },
154
172
  height: { type: Number },
155
- uploadId: { type: String }
173
+ upload: { type: Upload }
156
174
  }
157
175
  },
158
176
  purpose: {
@@ -170,16 +188,21 @@ definition.action({
170
188
  },
171
189
  /// TODO: accessControl!
172
190
  waitForEvents: true,
173
- async execute({ name, original, purpose }, { client, service }, emit) {
174
- const image = app.generateUid()
175
- const upload = await app.dao.get(['database', 'tableObject', app.databaseName, 'uploads', original.uploadId])
176
-
177
- if(!upload) throw new Error("upload_not_found")
178
- if(upload.state!='done') throw new Error("upload_not_done")
191
+ async execute({ image, name, original, purpose, owner, ownerType }, { client, service }, emit) {
192
+ if(!image) {
193
+ image = app.generateUid()
194
+ } else {
195
+ // TODO: check id source session
196
+ const existing = await Image.get(image)
197
+ if (existing) throw 'already_exists'
198
+ }
199
+ const uploadRow = await Upload.get(original.upload)
200
+ if(!uploadRow) throw new Error("upload_not_found")
201
+ if(uploadRow.state!='done') throw new Error("upload_not_done")
179
202
 
180
- let extension = upload.fileName.match(/\.([A-Z0-9]+)$/i)[1].toLowerCase()
203
+ let extension = uploadRow.fileName.match(/\.([A-Z0-9]+)$/i)[1].toLowerCase()
181
204
  if(extension == 'jpg') extension = "jpeg"
182
- const dir = `${storageDir}${image}`
205
+ const dir = `${imagesPath}${image}`
183
206
 
184
207
  emit({
185
208
  type: "ownerOwnedImageCreated",
@@ -190,7 +213,7 @@ definition.action({
190
213
  data: {
191
214
  name,
192
215
  purpose,
193
- fileName: upload.fileName,
216
+ fileName: uploadRow.fileName,
194
217
  original: {
195
218
  width: original.width,
196
219
  height: original.height,
@@ -204,8 +227,12 @@ definition.action({
204
227
  await mkdir(dir)
205
228
  await mkdir(`${dir}/originalCache`)
206
229
  await mkdir(`${dir}/cropCache`)
207
- await move(`${uploadsDir}${upload.id}`, `${dir}/original.${extension}`)
208
- await app.dao.request(['database', 'delete'], app.databaseName, 'uploads', upload.id)
230
+ await move(`${uploadsPath}${uploadRow.id}`, `${dir}/original.${extension}`)
231
+
232
+ await app.trigger({
233
+ type: 'uploadUsed',
234
+ upload: uploadRow.id
235
+ })
209
236
 
210
237
  return image
211
238
  }
@@ -228,29 +255,29 @@ definition.action({
228
255
  orientation: {type: Number}
229
256
  }
230
257
  },
231
- uploadId: {type: String}
258
+ upload: {type: Upload}
232
259
  },
233
260
  /// TODO: accessControl!
234
261
  waitForEvents: true,
235
- async execute({ image, crop, uploadId }, {client, service}, emit) {
262
+ async execute({ image, crop, upload }, {client, service}, emit) {
236
263
  const imageRow = await Image.get(image)
237
264
  if(!imageRow) throw new Error("not_found")
238
265
 
239
- const upload = await app.dao.get(['database', 'tableObject', app.databaseName, 'uploads', uploadId])
266
+ const uploadRow = await Upload.get(upload)
240
267
 
241
- console.log("UPLOAD CROP", upload)
268
+ console.log("UPLOAD CROP", uploadRow)
242
269
 
243
- if(!upload) throw new Error("upload_not_found")
244
- if(upload.state != 'done') throw new Error("upload_not_done")
270
+ if(!uploadRow) throw new Error("upload_not_found")
271
+ if(uploadRow.state != 'done') throw new Error("upload_not_done")
245
272
 
246
273
  console.log("CURRENT IMAGE ROW", image, imageRow)
247
274
  if(!imageRow.crop) { // first crop
248
- const dir = `${storageDir}${image}`
249
- let extension = upload.fileName.match(/\.([A-Z0-9]+)$/i)[1].toLowerCase()
275
+ const dir = `${imagesPath}${image}`
276
+ let extension = uploadRow.fileName.match(/\.([A-Z0-9]+)$/i)[1].toLowerCase()
250
277
  if(extension == 'jpg') extension = "jpeg"
251
278
 
252
- await move(`${uploadsDir}${upload.id}`, `${dir}/crop.${extension}`)
253
- await app.dao.request(['database', 'delete'], app.databaseName, 'uploads', upload.id)
279
+ await move(`${uploadsPath}${uploadRow.id}`, `${dir}/crop.${extension}`)
280
+ await app.dao.request(['database', 'delete'], app.databaseName, 'uploads', uploadRow.id)
254
281
 
255
282
  emit({
256
283
  type: "ownerOwnedImageUpdated",
@@ -264,8 +291,8 @@ definition.action({
264
291
  } else { // next crop - need to copy image
265
292
  const newImage = app.generateUid()
266
293
 
267
- const dir = `${storageDir}${image}`
268
- const newDir = `${storageDir}${newImage}`
294
+ const dir = `${imagesPath}${image}`
295
+ const newDir = `${imagesPath}${newImage}`
269
296
 
270
297
  await mkdir(newDir)
271
298
  await mkdir(`${newDir}/originalCache`)
@@ -273,11 +300,15 @@ definition.action({
273
300
  await move(`${dir}/original.${imageRow.original.extension}`,
274
301
  `${newDir}/original.${imageRow.original.extension}`)
275
302
 
276
- let extension = upload.fileName.match(/\.([A-Z0-9]+)$/i)[1].toLowerCase()
303
+ let extension = uploadRow.fileName.match(/\.([A-Z0-9]+)$/i)[1].toLowerCase()
277
304
  if(extension == 'jpg') extension = "jpeg"
278
305
 
279
- await move(`../../storage/uploads/${upload.id}`, `${newDir}/crop.${extension}`)
280
- await app.dao.request(['database', 'delete'], app.databaseName, 'uploads', upload.id)
306
+ await move(`../../storage/uploads/${uploadRow.id}`, `${newDir}/crop.${extension}`)
307
+
308
+ await app.trigger({
309
+ type: 'uploadUsed',
310
+ upload: uploadRow.id
311
+ })
281
312
 
282
313
  const { owner, ownerType } = imageRow
283
314
 
@@ -290,7 +321,7 @@ definition.action({
290
321
  data: {
291
322
  name: imageRow.name,
292
323
  purpose: imageRow.purpose,
293
- fileName: upload.fileName,
324
+ fileName: uploadRow.fileName,
294
325
  original: imageRow.original,
295
326
  crop
296
327
  }
@@ -333,8 +364,8 @@ definition.trigger({
333
364
  async execute({ name, purpose, url, cropped, owner, ownerType }, { service, client }, emit) {
334
365
  const image = app.generateUid()
335
366
 
336
- const downloadPath = `${uploadsDir}download_${image}`
337
- await download(url, uploadsDir, { filename: `download_${image}` })
367
+ const downloadPath = `${uploadsPath}download_${image}`
368
+ await download(url, uploadsPath, { filename: `download_${image}` })
338
369
 
339
370
  const metadata = await sharp(downloadPath).metadata()
340
371
 
@@ -376,7 +407,7 @@ definition.trigger({
376
407
  data
377
408
  })
378
409
 
379
- const dir = `${storageDir}/${image}`
410
+ const dir = `${imagesPath}/${image}`
380
411
 
381
412
  await mkdir(dir)
382
413
  await mkdir(`${dir}/originalCache`)
package/index.js CHANGED
@@ -2,6 +2,8 @@ const app = require("@live-change/framework").app()
2
2
 
3
3
  const definition = require('./definition.js')
4
4
 
5
+ require('./clientConfig.js')
5
6
  require('./image.js')
7
+ require('./endpoint.js')
6
8
 
7
9
  module.exports = definition
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@live-change/image-service",
3
- "version": "0.2.29",
3
+ "version": "0.2.34",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -21,9 +21,11 @@
21
21
  "url": "https://www.viamage.com/"
22
22
  },
23
23
  "dependencies": {
24
- "@live-change/framework": "0.6.3",
25
- "@live-change/relations-plugin": "0.6.3",
26
- "pluralize": "8.0.0"
24
+ "@live-change/framework": "0.6.5",
25
+ "@live-change/relations-plugin": "0.6.5",
26
+ "download": "^8.0.0",
27
+ "pluralize": "8.0.0",
28
+ "sharp": "^0.30.6"
27
29
  },
28
- "gitHead": "37d229ac05adf5e045ae3dcc826c6945d5dc3670"
30
+ "gitHead": "45f52d6c7586d0eaa51b3205c6635a33e32eef6b"
29
31
  }