Skip to content
This repository has been archived by the owner on Mar 5, 2021. It is now read-only.

[WIP] Provide an easy way for user to import a project.. #150

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions client/src/hub/index.jade
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,13 @@ html
.projects-header
.projects-buttons.button-strip
button(disabled).new-project= t("hub:newProject.title")
button(disabled).import-project= t("hub:importProject.title")
button(disabled).open-project= t("hub:openProject")
button(disabled).edit-project= t("hub:editDetails.title")

form(hidden)
input.project-archive-select(type="file", accept="application/zip")

.language-container
span= t("hub:language")
select.language
Expand Down
18 changes: 17 additions & 1 deletion client/src/hub/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ function start() {
document.querySelector(".projects-buttons .new-project").addEventListener("click", onNewProjectClick);
document.querySelector(".projects-buttons .open-project").addEventListener("click", onProjectActivate);
document.querySelector(".projects-buttons .edit-project").addEventListener("click", onEditProjectClick);
const importedFile = document.querySelector("input.project-archive-select") as HTMLInputElement;
importedFile.addEventListener("change", onImportProjectClick);
document.querySelector(".projects-buttons .import-project").addEventListener("click", () => { importedFile.click(); });

const selectLanguageElt = document.querySelector("select.language") as HTMLSelectElement;
const languageIds = Object.keys(languageNamesById);
Expand Down Expand Up @@ -102,8 +105,9 @@ function onConnected() {

const buttons = document.querySelectorAll(".projects-buttons button") as NodeListOf<HTMLButtonElement>;
buttons[0].disabled = false;
buttons[1].disabled = false;
const noSelection = ui.projectsTreeView.selectedNodes.length === 0;
for (let i = 1; i < buttons.length; i++) buttons[i].disabled = noSelection;
for (let i = 2; i < buttons.length; i++) buttons[i].disabled = noSelection;
}

function onHubWelcome(config: { serverName: string; }) {
Expand Down Expand Up @@ -243,3 +247,15 @@ function onEditProjectClick() {
});
});
}

function onImportProjectClick(event: Event) {
const target = event.target as HTMLInputElement;
if (target.files.length === 0) return;

const reader = new FileReader();

reader.onload = function(evnt) {
socket.emit("import:projects", { name: target.files[0].name.split(".")[0], data: (evnt.target as any).result });
};
reader.readAsArrayBuffer(target.files[0]);
}
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
"async-lock": "^0.3.8",
"body-parser": "^1.15.2",
"cookie-parser": "^1.4.1",
"copy-dir": "^0.3.0",
"dnd-tree-view": "^3.1.2",
"express": "^4.13.3",
"express-session": "^1.13.0",
Expand All @@ -55,6 +56,7 @@
"rimraf": "^2.5.0",
"socket.io": "^1.4.4",
"tab-strip": "^2.3.0",
"temp": "^0.8.3",
"tv4": "^1.2.7",
"yargs": "^3.32.0",
"yauzl": "^2.4.1"
Expand Down
4 changes: 4 additions & 0 deletions public/locales/en/hub.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,9 @@
"prompt": "Edit the project's details."
},
"openProject": "Open project",
"importProject": {
"title": "Import project",
"prompt": "Import a project from a ZIP archive"
},
"language": "Language"
}
4 changes: 4 additions & 0 deletions public/locales/fr/hub.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,9 @@
"prompt": "Éditez les informations du projet."
},
"openProject": "Ouvrir le projet",
"importProject": {
"title": "Importez un projet",
"prompt": "Importez un projet en archive ZIP"
},
"language": "Langue"
}
4 changes: 2 additions & 2 deletions server/ProjectHub.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ export default class ProjectHub {
}, callback);
}

loadProject(folderName: string, callback: (err: Error) => any) {
loadProject(folderName: string, callback: (err: Error, manifest?: SupCore.Data.ProjectManifest) => any) {
const server = new ProjectServer(this.globalIO, `${this.projectsPath}/${folderName}`, this.buildsPath, (err) => {
if (err != null) { callback(err); return; }

Expand All @@ -72,7 +72,7 @@ export default class ProjectHub {
}

this.serversById[server.data.manifest.pub.id] = server;
callback(null);
callback(null, server.data.manifest);
});
}

Expand Down
68 changes: 68 additions & 0 deletions server/RemoteHubClient.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
import * as async from "async";
import * as fs from "fs";
import * as path from "path";
import * as mkdirp from "mkdirp";

import { server as serverConfig } from "./config";
import ProjectHub from "./ProjectHub";
import BaseRemoteClient from "./BaseRemoteClient";

/* tslint:disable */
const yauzl = require("yauzl");
const temp = require("temp").track();
const copydir = require("copy-dir");
/* tslint:enable */

interface ProjectDetails {
name: string;
description: string;
Expand All @@ -14,6 +21,10 @@ interface ProjectDetails {
icon: Buffer;
}
interface AddProjectCallback { (err: string, projectId?: string): any; };
interface ImportProjectData {
name: string;
data: string;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should be a Buffer not a string? Since it's an ArrayBuffer on the client-side, should be converted to a NodeJS Bon the server-side.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oups, it was an inattention mistake. I change this!

};

export default class RemoteHubClient extends BaseRemoteClient {
constructor(public server: ProjectHub, socket: SocketIO.Socket) {
Expand All @@ -24,6 +35,7 @@ export default class RemoteHubClient extends BaseRemoteClient {
// Projects
this.socket.on("add:projects", this.onAddProject);
this.socket.on("edit:projects", this.onEditProject);
this.socket.on("import:projects", this.onImportProject);
}

// TODO: Implement roles and capabilities
Expand Down Expand Up @@ -190,4 +202,60 @@ export default class RemoteHubClient extends BaseRemoteClient {
else callback(null);
});
};

private onImportProject = (data: ImportProjectData) => {
// Import a project with a zip archive
// 1] Extract the content of the zip file in a temporary folder
// 2] Move the extracted content into the projects' folder

// Extract the content of the zip file in a temporary folder
temp.mkdir("project", (err: Error, dirPath: any) => {
const inputPath = path.join(dirPath, "project.zip");
let rootFolderName: string = null;
fs.writeFile(inputPath, data.data, (err) => {
if (err) throw err;

yauzl.open(inputPath, { lazyEntries: true }, (err: Error, zipFile: any) => {
if (err != null) throw err;

zipFile.readEntry();
zipFile.on("entry", (entry: any) => {
if (rootFolderName === null) {
rootFolderName = entry.fileName;
}

if (/\/$/.test(entry.fileName)) {
mkdirp(path.join(dirPath, entry.fileName), (err: Error) => {
if (err != null) throw err;
zipFile.readEntry();
});
}
else {
zipFile.openReadStream(entry, (err: Error, readStream: NodeJS.ReadableStream) => {
if (err != null) throw err;

mkdirp(path.dirname(path.join(dirPath, entry.fileName)), (err: Error) => {
if (err != null) throw err;

readStream.pipe(fs.createWriteStream(path.join(dirPath, entry.fileName)));
readStream.on("end", () => {
zipFile.readEntry();
});
});
});
}
});
zipFile.on("end", () => {
// move the extracted content into the projects' folder
copydir(path.join(dirPath, rootFolderName), path.join(this.server.projectsPath, rootFolderName), (err: Error) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any reason why we want to copy rather than just doing fs.rename on the root dir? Seems better to avoid having a copy lying around, even if it's in a temp folder.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, when I tried with fs.rename I had a crash on the server side with on error like this:

Error: EXDEV: cross-device link not permitted, rename '/tmp/project1161019-14225-1cpphb6/dodgell-master/' -> '/home/ilphrin/Jeux/superpowers/core/projects/dodgell-master/'

I didn't find a solution to this, so I tried copying the folder and then it worked

if (err != null) throw err;
this.server.loadProject(rootFolderName, (err: Error, manifest: SupCore.Data.ProjectManifest) => {
if (err != null) throw err;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here we still need to:

  • Add the project manifest to this.server.projects (see here:
    this.server.data.projects.add(manifest, sortedIndex, (err: string, actualIndex: number) => {
    ). This will make it appear in the list when people reload the page.
  • And then we need to notify all people already connected to the hub of the new project (see
    this.server.io.in("sub:projects").emit("add:projects", manifest, actualIndex);
    ).

Otherwise the new project will be imported but you'll need to restart the server before it can be joined.

});
});
});
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Regarding #46 (comment):

I think you'll need to change loadProject in ProjectHub.ts to pass you the newly initialized project server as a second argument, then you can access the manifest (which contains name, description, etc. to send it to clients for display in the hub).

For actualIndex, I think after loading the project, we'll need to add the ProjectHub's data.projects so that it shows up in the list, and that will give you that index. If that's too confusing I can take care of it.

});
});
}
}