Skip to content

Commit

Permalink
Implement git-based version history
Browse files Browse the repository at this point in the history
  • Loading branch information
mohsin-r committed Nov 15, 2024
1 parent 483a170 commit a66f87a
Show file tree
Hide file tree
Showing 6 changed files with 237 additions and 38 deletions.
Binary file removed server/files/884889330141e08c53e860b00.zip
Binary file not shown.
103 changes: 96 additions & 7 deletions server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ var cors = require('cors');
var moment = require('moment'); // require
const decompress = require('decompress');
const archiver = require('archiver');
const simpleGit = require('simple-git');
const responseMessages = [];
require('dotenv').config();

Expand Down Expand Up @@ -43,6 +44,19 @@ app.use(cors());

// POST requests made to /upload will be handled here.
app.route(ROUTE_PREFIX + '/upload').post(function (req, res, next) {
// TODO: Putting this on the header isn't great. The body has the zipped folder. And usernames in the URL doesn't look great either. Maybe improve this somehow.
const user = req.headers.user
if (!user) {
// Send back error if the user uploading the storyline was not provided.
responseMessages.push({
type: 'WARNING',
message: 'Upload Aborted: the user uploading the form was not provided.'
});
logger('WARNING', 'Upload Aborted: the user uploading the form was not provided.');
res.status(400).send({ status: 'Bad Request' });
return;
}

const options = {
uploadDir: UPLOAD_PATH,
keepExtensions: true,
Expand All @@ -56,7 +70,7 @@ app.route(ROUTE_PREFIX + '/upload').post(function (req, res, next) {
//const projectNameRegex = /[a-zA-Z0-9]{8}(-[a-zA-Z0-9]{4}){3}-[a-zA-Z0-9]{12}/g;

// Upload the file to the server, into the /files/ folder.
form.parse(req, function (err, field, file) {
form.parse(req, async function (err, field, file) {
if (err) {
responseMessages.push({
type: 'WARNING',
Expand Down Expand Up @@ -92,12 +106,15 @@ app.route(ROUTE_PREFIX + '/upload').post(function (req, res, next) {
responseMessages.push({ type: 'INFO', message: `Successfully created new product ${fileName}` });
logger('INFO', `Successfully created new product ${fileName}`);
fs.mkdirSync(fileName);
// Initialize a new git repo in the storyline's folder
const git = simpleGit(fileName);
await git.init();
newStorylines = true;
}

// Unzip the contents of the uploaded zip file into the target directory. Will overwrite
// old files in the folder.
decompress(secureFilename, fileName).then((files) => {
decompress(secureFilename, fileName).then(async () => {
// SECURITY FEATURE: delete all files in the folder that don't have one of the following extensions:
// .json, .jpg, .jpeg, .gif, .png, .csv
// TODO: Disabled until I can find a better regex
Expand All @@ -107,25 +124,67 @@ app.route(ROUTE_PREFIX + '/upload').post(function (req, res, next) {
responseMessages.push({ type: 'INFO', message: `Uploaded files to product ${fileName}` });
logger('INFO', `Uploaded files to product ${fileName}`);

const date = moment().format('YYYY-MM-DD')
const time = moment().format('hh:mm:ss a')
// Initialize git
const git = simpleGit(fileName);
let versionNumber = 1;
// We check if this is not the first save, otherwise the git rev-list command will error
if (!newStorylines) {
// Compute version number for storyline
versionNumber = await git.raw('rev-list', '--count', 'HEAD');
versionNumber = Number(versionNumber[0]) + 1;
}
// Commit the files for this storyline to its repo
await git.add('./*').commit(`Add product version ${versionNumber} on ${date} at ${time}`, {'--author': `"${user} <>"`})
// Finally, delete the uploaded zip file.
safeRM(secureFilename, UPLOAD_PATH);

// Get the hash of the latest commit
const commits = await git.log();
const lastHash = commits.latest.hash;
// Send a response back to the client.
res.json({ new: newStorylines });
res.json({ new: newStorylines, commitHash: lastHash });
});
});
});

// GET requests made to /retrieve/ID will be handled here.
app.route(ROUTE_PREFIX + '/retrieve/:id').get(function (req, res, next) {
// GET requests made to /retrieve/ID/commitHash will be handled here.
// Callimg this with commitHash as "latest" simply fetches the product as normal.
app.route(ROUTE_PREFIX + '/retrieve/:id/:hash').get(function (req, res, next) {
var archive = archiver('zip');
const PRODUCT_PATH = `${TARGET_PATH}/${req.params.id}`;
const uploadLocation = `${UPLOAD_PATH}/${req.params.id}-outgoing.zip`;
const commitHash = req.params.hash

// Check if the product exists.
if (
fs.access(PRODUCT_PATH, (error) => {
fs.access(PRODUCT_PATH, async (error) => {
if (!error) {
const git = simpleGit(PRODUCT_PATH);
if (commitHash !== 'latest') {
// If the user does not ask for the latest commit, we checkout a new branch at the point of the requested commit,
// and then proceed with getting the zipped folder below.
try {
// First, we check if the requested commit exists.
// NOTE: When calling from frontend, the catch block should never run.
const commitExists = await git.catFile(['-t', commitHash]);
if (commitExists !== 'commit\n') {
throw new Error()
}
} catch (error) {
responseMessages.push({
type: 'INFO',
message: `Access attempt to version ${commitHash} of product ${req.params.id} failed, does not exist.`
});
logger('INFO', `Access attempt to version ${commitHash} of product ${req.params.id} failed, does not exist.`);
res.status(404).send({ status: 'Not Found' });
return;
}
// Checkout a new branch at the point of the requested commit
// This will result in the code below returning the version's folder back to the client.
await git.checkoutBranch(`version-${commitHash}`, commitHash);
}

const output = fs.createWriteStream(uploadLocation);
// This event listener is fired when the write stream has finished. This means that the
// ZIP file should be correctly populated. Now, we can set the correct headers and send the
Expand Down Expand Up @@ -155,6 +214,13 @@ app.route(ROUTE_PREFIX + '/retrieve/:id').get(function (req, res, next) {

responseMessages.push({ type: 'INFO', message: `Successfully loaded product ${req.params.id}` });
logger('INFO', `Successfully loaded product ${req.params.id}`);

if (commitHash !== 'latest') {
// Since the user has not asked for the latest commit, we need to clean up.
// Go back to the main branch and delete the newly created branch.
await git.checkout('main');
await git.deleteLocalBranch(`version-${commitHash}`)
}
} else {
responseMessages.push({
type: 'INFO',
Expand Down Expand Up @@ -209,6 +275,29 @@ app.route(ROUTE_PREFIX + '/retrieve/:id/:lang').get(function (req, res) {
);
});

app.route(ROUTE_PREFIX + '/history/:id').get(function (req, res, next) {
const PRODUCT_PATH = `${TARGET_PATH}/${req.params.id}`;
// Check if the product exists.
fs.access(PRODUCT_PATH, async (error) => {
if (error) {
responseMessages.push({
type: 'INFO',
message: `Access attempt to versions of ${req.params.id} failed, does not exist.`
});
logger('INFO', `Access attempt to versions of ${req.params.id} failed, does not exist.`);
res.status(404).send({ status: 'Not Found' });
}
else {
// Get version history for this product via git log command
const git = simpleGit(PRODUCT_PATH);
const log = await git.log()
const history = log.all.map((commit) => ({hash: commit.hash, created: commit.date, storylineUUID: req.params.id}))
res.json(history)
}
})

})

// GET reuests made to /retrieveMessages will recieve all the responseMessages currently queued.
app.route(ROUTE_PREFIX + '/retrieveMessages').get(function (req, res) {
res.json({ messages: responseMessages });
Expand Down
79 changes: 78 additions & 1 deletion server/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"fs-extra": "^11.1.0",
"moment": "^2.29.4",
"path": "^0.12.7",
"recursive-readdir": "^2.2.3"
"recursive-readdir": "^2.2.3",
"simple-git": "^3.27.0"
}
}
Loading

0 comments on commit a66f87a

Please sign in to comment.