issue/37 #42

Merged
dbroqua merged 2 commits from issue/37 into develop 2022-04-09 00:07:22 +02:00
11 changed files with 231 additions and 34 deletions
Showing only changes of commit d02cfcf94e - Show all commits

View file

@ -184,6 +184,26 @@ server {
Une fois le vhost activé (lien symbolique dans le dossier site-enable) et nginx rechargé votre site sera alors accessible en https. Une fois le vhost activé (lien symbolique dans le dossier site-enable) et nginx rechargé votre site sera alors accessible en https.
### Jobs
Par défaut toute les images des albums sont affichées depuis Discogs. Cependant avec les temps les urls deviennent invalides. Pour éviter cela lors de l'ajout d'un album à votre collection un job est créé. Ce job a pour rôle de stocker les images sur un bucket s3.
Pour lancer les jobs il faut mettre en place une tâche cron qui sera éxécutée toute les heures (par exemple).
Exemple de crontab :
```crontab
0 * * * * curl 'http://localhost:3001/jobs' \
-H 'JOBS_HEADER_KEY: JOBS_HEADER_VALUE' \
-H 'Accept: application/json'
30 * * * * curl 'http://localhost:3001/jobs?state=ERROR' \
-H 'JOBS_HEADER_KEY: JOBS_HEADER_VALUE' \
-H 'Accept: application/json'
```
N'oubliez pas de remplacer `localhost:30001`, `JOBS_HEADER_KEY` et `JOBS_HEADER_VALUE` par les bonnes valeurs.
La première ligne permet de parcourir tous les nouveaux jobs alors que la seconde permet de relancer les jobs en erreurs (après 5 tentatives le job est marqué comme définitivement perdu).
### Fichier .env {#env-file} ### Fichier .env {#env-file}
Voici la liste des variables configurables : Voici la liste des variables configurables :
@ -204,6 +224,8 @@ S3_ENDPOINT # Url de l'instance aws (s3.fr-par.scw.cloud pour scaleway france pa
S3_SIGNATURE # Version de la signature AWS (s3v4 pour scaleway par exemple) S3_SIGNATURE # Version de la signature AWS (s3v4 pour scaleway par exemple)
S3_BASEFOLDER # Nom du sous dossier dans lequel seront mis les pochettes des albums S3_BASEFOLDER # Nom du sous dossier dans lequel seront mis les pochettes des albums
S3_BUCKET # Nom du bucket S3_BUCKET # Nom du bucket
JOBS_HEADER_KEY # Nom du header utilisé pour l'identification des tâches cron (par exemple musictopus)
JOBS_HEADER_VALUE # Valeur de la clé
``` ```
## Contributeurs ## Contributeurs

View file

@ -34,6 +34,8 @@ services:
S3_BUCKET: ${S3_BUCKET} S3_BUCKET: ${S3_BUCKET}
S3_ENDPOINT: ${S3_ENDPOINT} S3_ENDPOINT: ${S3_ENDPOINT}
S3_SIGNATURE: ${S3_SIGNATURE} S3_SIGNATURE: ${S3_SIGNATURE}
JOBS_HEADER_KEY: ${JOBS_HEADER_KEY}
JOBS_HEADER_VALUE: ${JOBS_HEADER_VALUE}
networks: networks:
- musictopus - musictopus
musictopus-db: musictopus-db:

View file

@ -34,6 +34,8 @@ services:
S3_BUCKET: ${S3_BUCKET} S3_BUCKET: ${S3_BUCKET}
S3_ENDPOINT: ${S3_ENDPOINT} S3_ENDPOINT: ${S3_ENDPOINT}
S3_SIGNATURE: ${S3_SIGNATURE} S3_SIGNATURE: ${S3_SIGNATURE}
JOBS_HEADER_KEY: ${JOBS_HEADER_KEY}
JOBS_HEADER_VALUE: ${JOBS_HEADER_VALUE}
networks: networks:
- musictopus - musictopus
musictopus-db: musictopus-db:

View file

@ -61,6 +61,7 @@
"mongoose-unique-validator": "^3.0.0", "mongoose-unique-validator": "^3.0.0",
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"passport": "^0.5.2", "passport": "^0.5.2",
"passport-custom": "^1.1.1",
"passport-http": "^0.3.0", "passport-http": "^0.3.0",
"passport-local": "^1.0.0", "passport-local": "^1.0.0",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",

View file

@ -7,6 +7,8 @@ import flash from "connect-flash";
import session from "express-session"; import session from "express-session";
import MongoStore from "connect-mongo"; import MongoStore from "connect-mongo";
import passportConfig from "./libs/passport";
import config, { env, mongoDbUri, secret } from "./config"; import config, { env, mongoDbUri, secret } from "./config";
import { isXhr } from "./helpers"; import { isXhr } from "./helpers";
@ -15,15 +17,13 @@ import indexRouter from "./routes";
import maCollectionRouter from "./routes/ma-collection"; import maCollectionRouter from "./routes/ma-collection";
import collectionRouter from "./routes/collection"; import collectionRouter from "./routes/collection";
import importJobsRouter from "./routes/jobs";
import importAlbumRouterApiV1 from "./routes/api/v1/albums"; import importAlbumRouterApiV1 from "./routes/api/v1/albums";
import importSearchRouterApiV1 from "./routes/api/v1/search"; import importSearchRouterApiV1 from "./routes/api/v1/search";
import importMeRouterApiV1 from "./routes/api/v1/me"; import importMeRouterApiV1 from "./routes/api/v1/me";
// Mongoose schema init passportConfig(passport);
require("./models/users");
require("./models/albums");
require("./libs/passport")(passport);
mongoose mongoose
.connect(mongoDbUri, { useNewUrlParser: true, useUnifiedTopology: true }) .connect(mongoDbUri, { useNewUrlParser: true, useUnifiedTopology: true })
@ -85,6 +85,7 @@ app.use(
app.use("/", indexRouter); app.use("/", indexRouter);
app.use("/ma-collection", maCollectionRouter); app.use("/ma-collection", maCollectionRouter);
app.use("/collection", collectionRouter); app.use("/collection", collectionRouter);
app.use("/jobs", importJobsRouter);
app.use("/api/v1/albums", importAlbumRouterApiV1); app.use("/api/v1/albums", importAlbumRouterApiV1);
app.use("/api/v1/search", importSearchRouterApiV1); app.use("/api/v1/search", importSearchRouterApiV1);
app.use("/api/v1/me", importMeRouterApiV1); app.use("/api/v1/me", importMeRouterApiV1);

View file

@ -14,4 +14,7 @@ module.exports = {
s3Bucket: process.env.S3_BUCKET || "musictopus", s3Bucket: process.env.S3_BUCKET || "musictopus",
s3Endpoint: process.env.S3_ENDPOINT || "s3.fr-par.scw.cloud", s3Endpoint: process.env.S3_ENDPOINT || "s3.fr-par.scw.cloud",
s3Signature: process.env.S3_SIGNATURE || "s3v4", s3Signature: process.env.S3_SIGNATURE || "s3v4",
jobsHeaderKey: process.env.JOBS_HEADER_KEY || "musictopus",
jobsHeaderValue:
process.env.JOBS_HEADER_VALUE || "ooYee9xok7eigo2shiePohyoGh1eepew",
}; };

View file

@ -1,11 +1,13 @@
/* eslint-disable func-names */ /* eslint-disable func-names */
const mongoose = require("mongoose"); import { Strategy as LocalStrategy } from "passport-local";
const LocalStrategy = require("passport-local").Strategy; import { BasicStrategy } from "passport-http";
const { BasicStrategy } = require("passport-http"); import { Strategy as CustomStrategy } from "passport-custom";
const Users = mongoose.model("Users"); import Users from "../models/users";
module.exports = function (passport) { import { jobsHeaderKey, jobsHeaderValue } from "../config";
export default (passport) => {
passport.serializeUser((user, done) => { passport.serializeUser((user, done) => {
done(null, user); done(null, user);
}); });
@ -55,4 +57,17 @@ module.exports = function (passport) {
.catch(done); .catch(done);
}) })
); );
passport.use(
"jobs",
new CustomStrategy((req, next) => {
const apiKey = req.headers[jobsHeaderKey];
if (apiKey === jobsHeaderValue) {
return next(null, {
username: "jobs",
});
}
return next(null, false, "Oops! Identifiants incorrects");
})
);
}; };

View file

@ -1,11 +1,10 @@
/* eslint-disable no-await-in-loop */
import moment from "moment"; import moment from "moment";
import Pages from "./Pages"; import Pages from "./Pages";
import Export from "./Export"; import Export from "./Export";
import AlbumsModel from "../models/albums"; import AlbumsModel from "../models/albums";
import CronModel from "../models/cron"; import JobsModel from "../models/jobs";
import UsersModel from "../models/users"; import UsersModel from "../models/users";
import ErrorEvent from "../libs/error"; import ErrorEvent from "../libs/error";
// import { uploadFromUrl } from "../libs/aws"; // import { uploadFromUrl } from "../libs/aws";
@ -31,34 +30,18 @@ class Albums extends Pages {
: null; : null;
delete data.id; delete data.id;
// INFO: {POC} Pour chaque image on récupère une version que l'on stocke localement
// Utiliser un cron qui check la librairie pour mettre à jour les urls des images
// Mettre en cron l'id du nouvel élément créé pour me pas parser toute la bibliothèque à chaque fois
// if (data.thumb) {
// data.thumb = await uploadFromUrl(data.thumb);
// data.thumbType = "local";
// }
// if (data.images && data.images.length > 0) {
// for (let i = 0; i < data.images.length; i += 1) {
// data.images[i].uri150 = await uploadFromUrl(
// data.images[i].uri150
// );
// data.images[i].uri = await uploadFromUrl(data.images[i].uri);
// }
// }
const album = new AlbumsModel(data); const album = new AlbumsModel(data);
await album.save(); await album.save();
const cronData = { const jobData = {
model: "Albums", model: "Albums",
id: album._id, id: album._id,
}; };
const cron = new CronModel(cronData); const job = new JobsModel(jobData);
cron.save(); job.save();
return album; return album;
} }

128
src/middleware/Jobs.js Normal file
View file

@ -0,0 +1,128 @@
/* eslint-disable no-await-in-loop */
import ErrorEvent from "../libs/error";
import { uploadFromUrl } from "../libs/aws";
import { getAlbumDetails } from "../helpers";
import JobsModel from "../models/jobs";
import AlbumsModel from "../models/albums";
class Jobs {
/**
* Méthode permettant de télécharger toute les images d'un album
* @param {ObjectId} itemId
*/
static async importAlbumAssets(itemId) {
const album = await AlbumsModel.findById(itemId);
if (!album) {
throw new ErrorEvent(
404,
"Item non trouvé",
`L'album avant l'id ${itemId} n'existe plus dans la collection`
);
}
const item = await getAlbumDetails(album.discogsId);
if (!item) {
throw new ErrorEvent(
404,
"Erreur de communication",
"Erreur lors de la récupération des informations sur Discogs"
);
}
if (item.thumb) {
album.thumb = await uploadFromUrl(item.thumb);
album.thumbType = "local";
}
const { images } = item;
if (images && images.length > 0) {
for (let i = 0; i < images.length; i += 1) {
images[i].uri150 = await uploadFromUrl(images[i].uri150);
images[i].uri = await uploadFromUrl(images[i].uri);
}
}
album.images = images;
await album.save();
return true;
}
/**
* Point d'entrée
* @param {String} state
*
* @return {Object}
*/
async run(state = "NEW") {
const job = await JobsModel.findOne({
state,
tries: {
$lte: 5,
},
});
if (!job) {
return { message: "All jobs done" };
}
job.state = "IN-PROGRESS";
await job.save();
try {
switch (job.model) {
case "Albums":
await Jobs.importAlbumAssets(job.id);
break;
default:
throw new ErrorEvent(
500,
"Job inconnu",
`Le job avec l'id ${job._id} n'est pas un job valide`
);
}
job.state = "SUCCESS";
await job.save();
return this.run(state);
} catch (err) {
job.state = "ERROR";
job.lastTry = new Date();
job.lastErrorMessage = err.message;
job.tries += 1;
await job.save();
throw err;
}
}
/**
* Méthode permettant de créer tous les jobs
*
* @return {Object}
*/
static async populate() {
const albums = await AlbumsModel.find();
for (let i = 0; i < albums.length; i += 1) {
const jobData = {
model: "Albums",
id: albums[i]._id,
};
const job = new JobsModel(jobData);
await job.save();
}
return { message: `${albums.length} jobs ajouté à la file d'attente` };
}
}
export default Jobs;

View file

@ -2,13 +2,13 @@ import mongoose from "mongoose";
const { Schema } = mongoose; const { Schema } = mongoose;
const CronSchema = new mongoose.Schema( const JobSchema = new mongoose.Schema(
{ {
model: String, model: String,
id: Schema.Types.ObjectId, id: Schema.Types.ObjectId,
state: { state: {
type: String, type: String,
enum: ["NEW", "ERROR", "SUCCESS"], enum: ["NEW", "IN-PROGRESS", "ERROR", "SUCCESS"],
default: "NEW", default: "NEW",
}, },
lastTry: Date, lastTry: Date,
@ -21,4 +21,4 @@ const CronSchema = new mongoose.Schema(
{ timestamps: true } { timestamps: true }
); );
export default mongoose.model("Cron", CronSchema); export default mongoose.model("Jobs", JobSchema);

40
src/routes/jobs.js Normal file
View file

@ -0,0 +1,40 @@
import express from "express";
import passport from "passport";
import Jobs from "../middleware/Jobs";
// eslint-disable-next-line new-cap
const router = express.Router();
router.route("/").get(
passport.authenticate(["jobs"], {
session: false,
}),
async (req, res, next) => {
try {
const job = new Jobs();
const data = await job.run(req.query.state);
return res.status(200).json(data).end();
} catch (err) {
return next(err);
}
}
);
router.route("/populate").get(
passport.authenticate(["jobs"], {
session: false,
}),
async (req, res, next) => {
try {
const data = await Jobs.populate();
return res.status(200).json(data).end();
} catch (err) {
return next(err);
}
}
);
export default router;