Skip to content

Commit 68c167d

Browse files
authored
Merge pull request #157 from julianpoy/feature/#150-living-cookbook-import
Feature/#150 Living cookbook import
2 parents 652d661 + 18aa36a commit 68c167d

File tree

20 files changed

+991
-172
lines changed

20 files changed

+991
-172
lines changed

Backend/app.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ var devMode = appConfig.environment === 'dev';
2121

2222
Raven.config(appConfig.sentry.dsn, {
2323
environment: appConfig.environment,
24-
release: '1.1.1'
24+
release: '1.5.0'
2525
}).install();
2626

2727
// Routes
@@ -46,8 +46,8 @@ app.set('views', path.join(__dirname, 'views'));
4646
app.set('view engine', 'pug');
4747

4848
if (!testMode) app.use(logger('dev'));
49-
app.use(bodyParser.json({limit: '4MB'}));
50-
app.use(bodyParser.urlencoded({ limit: '4MB', extended: false }));
49+
app.use(bodyParser.json({limit: '250MB'}));
50+
app.use(bodyParser.urlencoded({ limit: '250MB', extended: false }));
5151
app.use(cookieParser());
5252
app.disable('x-powered-by');
5353

Backend/package.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,12 @@
1717
"debug": "~2.6.9",
1818
"express": "~4.15.5",
1919
"express-grip": "^1.1.0",
20+
"extract-zip": "^1.6.7",
2021
"firebase-admin": "^5.9.1",
22+
"fs-extra": "^7.0.1",
23+
"gm": "^1.23.1",
2124
"grip": "^1.2.8",
25+
"mdb": "^0.1.0",
2226
"moment": "^2.20.1",
2327
"morgan": "~1.9.0",
2428
"multer": "^1.3.0",
@@ -39,7 +43,7 @@
3943
"mocha": "^5.2.0",
4044
"sequelize-cli": "^5.4.0",
4145
"sinon": "^7.2.2",
42-
"sqlite3": "^4.0.4",
46+
"sqlite3": "^4.0.6",
4347
"supertest": "^3.3.0"
4448
}
4549
}

Backend/routes/index.js

Lines changed: 309 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@ var router = express.Router();
33
let puppeteer = require('puppeteer');
44
var cors = require('cors');
55
var Raven = require('raven');
6+
let multer = require('multer');
7+
let fs = require('fs-extra');
8+
let mdb = require('mdb');
9+
let extract = require('extract-zip');
10+
let sqlite3 = require('sqlite3');
611

712
// DB
813
var Op = require("sequelize").Op;
@@ -212,4 +217,308 @@ router.get(
212217
}).catch(next);
213218
});
214219

220+
let tablesNeeded = [
221+
// "t_attachment", //2x unused
222+
"t_authornote", // seems to be a cross between description (short) and notes (long) - sometimes very long (multiple entries per recipe, divided paragraph)
223+
// "t_cookbook_x", // unused from this db afaik
224+
// "t_favorite_x", //2x unused
225+
// "t_favoritefolder", //2x unused
226+
// "t_glossaryitem",
227+
// "t_groceryaisle",
228+
// "t_grocerylistitemrecipe",
229+
"t_image", // Holds filenames for all images
230+
// "t_ingredient",
231+
// "t_ingredientattachment",
232+
// "t_ingredientautocomplete",
233+
// "t_ingredientfolder",
234+
// "t_ingredientfolder_x",
235+
// "t_ingredientimage",
236+
// "t_meal", // Holds meal names with an abbreviation. No reference to any other table
237+
// "t_measure",
238+
// "t_measure_x",
239+
// "t_menu", // Holds menu info - has some "types" info that might be useful for labelling
240+
// "t_menu_x", // unused
241+
// "t_menuimage",
242+
"t_recipe",
243+
// "t_recipe_x", //2x unused
244+
// "t_recipeattachment", // 2x unused
245+
"t_recipeimage", // bidirectional relation table between recipe and image
246+
"t_recipeingredient",
247+
// "t_recipemeasure",
248+
"t_recipeprocedure",
249+
// "t_recipereview",
250+
// "t_technique",
251+
// "t_recipetechnique",
252+
"t_recipetip",
253+
// "t_recipetype", // seems to store category names, but no discernable relationship to recipe table - better to use recipetypes field in recipe itself (comma separated)
254+
// "t_recipetype_x", //2x unused
255+
// "t_grocerylistitem",
256+
// "t_ingredient_x", //2x unused
257+
// "t_ingredientmeasure", //not entirely clear - looks like a relationship table between ingredients and measurements
258+
// "t_recipemedia" //2x unused (or barely used)
259+
]
260+
261+
router.post(
262+
'/import/livingcookbook',
263+
cors(),
264+
MiddlewareService.validateSession(['user']),
265+
MiddlewareService.validateUser,
266+
multer({
267+
dest: '/tmp/chefbook-lcb-import/',
268+
}).single('lcbdb'),
269+
async (req, res, next) => {
270+
if (!req.file) {
271+
res.status(400).send("Must include a file with the key lcbdb")
272+
} else {
273+
console.log(req.file.path)
274+
}
275+
276+
let sqliteDB;
277+
let lcbDB;
278+
let zipPath = req.file.path;
279+
let extractPath = zipPath + '-extract';
280+
let dbPath;
281+
let lcbTables;
282+
let schemaInitSQL;
283+
let tableInitSQL = {};
284+
let tableMap = {};
285+
286+
try {
287+
await new Promise((resolve, reject) => {
288+
sqliteDB = new sqlite3.Database(':memory:', (err) => {
289+
if (err) reject(err)
290+
else resolve()
291+
})
292+
});
293+
294+
return new Promise((resolve, reject) => {
295+
extract(zipPath, { dir: extractPath }, function (err) {
296+
if (err) reject(err);
297+
else resolve();
298+
})
299+
})
300+
.then(() => {
301+
fs.unlinkSync(zipPath)
302+
return UtilService.findFilesByRegex(extractPath, /\.mdb/i)
303+
})
304+
.then(potentialDbPaths => {
305+
if (potentialDbPaths.length == 0) return Promise.reject()
306+
307+
dbPath = potentialDbPaths[0];
308+
309+
if (potentialDbPaths.length > 1) {
310+
console.log("More than one lcbdb path - ", potentialDbPaths)
311+
Raven.captureMessage("More than one lcbdb path - ", potentialDbPaths);
312+
} else {
313+
Raven.captureMessage("LCB DB Path - ", dbPath)
314+
}
315+
316+
return dbPath
317+
}).then(dbPath => {
318+
// Load mdb
319+
lcbDB = mdb(dbPath);
320+
}).then(() => {
321+
// Load lcb schema
322+
return new Promise((resolve, reject) => {
323+
lcbDB.schema((mdbSchemaErr, result) => {
324+
if (mdbSchemaErr) reject(mdbSchemaErr);
325+
schemaInitSQL = result
326+
resolve()
327+
})
328+
})
329+
}).then(() => {
330+
// Load table insert statements
331+
return new Promise((resolve, reject) => {
332+
lcbDB.tables((err, tables) => {
333+
if (err) {
334+
throw err
335+
}
336+
lcbTables = tables
337+
338+
resolve()
339+
})
340+
}).then(() => Promise.all(
341+
lcbTables
342+
.filter(table => tablesNeeded.indexOf(table) !== -1)
343+
.map(table => new Promise((resolve, reject) =>
344+
lcbDB.toSQL(table, function (err, sql) {
345+
if (err && err !== "no output") return reject(err)
346+
if (!sql) return resolve()
347+
348+
// tableInitSQL[table] = sql.match(/[^\r\n]+/g);
349+
tableInitSQL[table] = sql.split(';\n'); // could be error prone
350+
351+
resolve()
352+
})
353+
))
354+
)
355+
)
356+
}).then(() => {
357+
// Send queries to database
358+
return new Promise((sqDbInitResolve, sqDbInitReject) => {
359+
sqliteDB.serialize(() => {
360+
let initStmts = schemaInitSQL.split(";");
361+
for (let i = 0; i < initStmts.length; i++) {
362+
let stmt = initStmts[i]
363+
364+
sqliteDB.run(stmt, (sqliteErr) => {
365+
if (sqliteErr && sqliteErr.code !== "SQLITE_MISUSE") throw sqliteErr
366+
})
367+
}
368+
369+
let tables = Object.keys(tableInitSQL)
370+
tables.forEach((tableName, idx) => {
371+
for (let i = 0; i < tableInitSQL[tableName].length; i++) {
372+
sqliteDB.run(tableInitSQL[tableName][i], (sqliteErr) => {
373+
if (sqliteErr && sqliteErr.code !== "SQLITE_MISUSE") {
374+
throw sqliteErr
375+
}
376+
377+
if (idx == tables.length - 1 && i == tableInitSQL[tableName].length - 1) {
378+
sqDbInitResolve()
379+
}
380+
})
381+
}
382+
})
383+
})
384+
})
385+
}).then(() => {
386+
return Promise.all(lcbTables.map(tableName => {
387+
return new Promise(resolve => {
388+
sqliteDB.all("SELECT * FROM " + tableName, [], (err, results) => {
389+
if (err) throw err;
390+
391+
tableMap[tableName] = results;
392+
393+
resolve();
394+
})
395+
})
396+
}))
397+
}).then(async () => {
398+
// return await fs.writeFile('output', JSON.stringify(tableMap))
399+
400+
return SQ.transaction(t => {
401+
return Promise.all((tableMap.t_recipe || []).filter(lcbRecipe => !!lcbRecipe.recipeid).map(lcbRecipe => {
402+
return new Promise(resolve => {
403+
let lcbImages = (tableMap.t_recipeimage || [])
404+
.filter(el => el.recipeid == lcbRecipe.recipeid)
405+
.sort((a, b) => a.imageindex.localeCompare(b.imageindex))
406+
407+
if (lcbImages.length == 0) return resolve()
408+
409+
let possibleFileNameRegex = [
410+
`recipe${lcbRecipe.recipeid}\.gif`,
411+
`recipe${lcbRecipe.recipeid}\.jpg`,
412+
`recipe${lcbRecipe.recipeid}\.jpeg`,
413+
`recipeimage${lcbRecipe.recipeid}\.gif`,
414+
`recipeimage${lcbRecipe.recipeid}\.jpg`,
415+
`recipeimage${lcbRecipe.recipeid}\.jpeg`
416+
].join('|')
417+
418+
let possibleImageFiles = UtilService.findFilesByRegex(extractPath, new RegExp(`(${possibleFileNameRegex})$`, 'i'))
419+
420+
if (possibleImageFiles.length == 0) return resolve()
421+
422+
UtilService.sendFileToS3(possibleImageFiles[0]).then(resolve).catch(() => {
423+
resolve(null)
424+
})
425+
}).then(image => {
426+
let ingredients = (tableMap.t_recipeingredient || [])
427+
.filter(el => el.recipeid == lcbRecipe.recipeid)
428+
.sort((a, b) => a.ingredientindex > b.ingredientindex)
429+
.map(lcbIngredient => `${lcbIngredient.quantitytext} ${lcbIngredient.unittext} ${lcbIngredient.ingredienttext}`)
430+
.join("\r\n")
431+
432+
let instructions = (tableMap.t_recipeprocedure || [])
433+
.filter(el => el.recipeid == lcbRecipe.recipeid)
434+
.sort((a, b) => a.procedureindex > b.procedureindex)
435+
.map(lcbProcedure => lcbProcedure.proceduretext)
436+
.join("\r\n")
437+
438+
let recipeTips = (tableMap.t_recipetip || [])
439+
.filter(el => el.recipeid == lcbRecipe.recipeid)
440+
.sort((a, b) => a.tipindex > b.tipindex)
441+
.map(lcbTip => lcbTip.tiptext)
442+
443+
let authorNotes = (tableMap.t_authornote || [])
444+
.filter(el => el.recipeid == lcbRecipe.recipeid)
445+
.sort((a, b) => a.authornoteindex > b.authornoteindex)
446+
.map(lcbAuthorNote => lcbAuthorNote.authornotetext)
447+
448+
let description = ''
449+
450+
let notes = []
451+
452+
// Add comments to notes
453+
if (lcbRecipe.comments) notes.push(lcbRecipe.comments)
454+
455+
// Add "author notes" to description or notes depending on length
456+
if (authorNotes.length == 1 && authorNotes[0].length <= 150) description = authorNotes[0]
457+
else if (authorNotes.length > 0) notes = [...notes, ...authorNotes]
458+
459+
// Add recipeTips and join with double return
460+
notes = [...notes, ...recipeTips].join('\r\n\r\n')
461+
462+
let createdAt = new Date(lcbRecipe.createdate || Date.now())
463+
let updatedAt = new Date(lcbRecipe.modifieddate || Date.now())
464+
465+
return Recipe.create({
466+
userId: res.locals.session.userId,
467+
title: lcbRecipe.recipename || '',
468+
description,
469+
yield: lcbRecipe.yield || '',
470+
activeTime: lcbRecipe.preparationtime || '',
471+
totalTime: lcbRecipe.readyintime || '',
472+
source: lcbRecipe.source || '',
473+
url: lcbRecipe.webpage || '',
474+
notes,
475+
ingredients,
476+
instructions,
477+
image: image,
478+
folder: 'main',
479+
fromUserId: null,
480+
createdAt,
481+
updatedAt
482+
}, { transaction: t }).then(recipe => {
483+
// Split, trim, tolowercase, filter nulls, then transform to set (remove dupes) and back to array
484+
let lcbRecipeLabels = [...new Set((lcbRecipe.recipetypes || '').split(',').map(el => el.trim().toLowerCase()).filter(el => el.length > 0))]
485+
486+
return Promise.all(lcbRecipeLabels.map(lcbLabelName => {
487+
return Label.findOrCreate({
488+
where: {
489+
userId: res.locals.session.userId,
490+
title: lcbLabelName
491+
},
492+
transaction: t
493+
}).then(function (labels) {
494+
return labels[0].addRecipe(recipe.id, { transaction: t });
495+
});
496+
}))
497+
})
498+
})
499+
})).then(() => {
500+
sqliteDB.close()
501+
fs.removeSync(zipPath)
502+
fs.removeSync(extractPath)
503+
504+
res.status(200).json({
505+
msg: "Success!"
506+
});
507+
})
508+
})
509+
}).catch(e => {
510+
fs.removeSync(zipPath)
511+
fs.removeSync(extractPath)
512+
console.log("Couldn't handle lcb upload 1", e)
513+
next(e);
514+
})
515+
} catch(e) {
516+
fs.removeSync(zipPath)
517+
fs.removeSync(extractPath)
518+
console.log("Couldn't handle lcb upload 2", e)
519+
next(e);
520+
}
521+
}
522+
)
523+
215524
module.exports = router;

0 commit comments

Comments
 (0)