Skip to content

Commit f2e38fe

Browse files
committed
Make server contemplate but not do GAF indexing, ban GAF uploads as unsupported, and allow upload errors to reach the user in an ugly way
1 parent a85a9a5 commit f2e38fe

File tree

7 files changed

+202
-37
lines changed

7 files changed

+202
-37
lines changed

docker/config.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@
8484
"fileTypeToExtensions": {
8585
"graph": ".xg,.vg,.hg,.gbz,.pg,.db",
8686
"haplotype": ".gbwt,.gbz",
87-
"read": ".gam,gaf.gz"
87+
"read": ".gam"
8888
},
8989

9090
"MAXUPLOADSIZE": 5242880,

exampleData/cactus-NA12879-small.gaf

+100
Large diffs are not rendered by default.

src/api/ServerAPI.mjs

+17-10
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ export class ServerAPI extends APIInterface {
111111
// Make sure server can identify a Read file
112112
formData.append("fileType", fileType);
113113

114-
return new Promise((resolve, reject) => {
114+
return await new Promise((resolve, reject) => {
115115
const xhr = new XMLHttpRequest();
116116
xhr.responseType = "json";
117117
xhr.onreadystatechange = () => {
@@ -121,21 +121,28 @@ export class ServerAPI extends APIInterface {
121121
reject(new Error("Upload aborted"));
122122
return;
123123
}
124-
124+
125125
if (xhr.readyState === 4) {
126126
if (xhr.status === 200 && xhr.response.path) {
127127
// Every thing ok, file uploaded, and we got a path.
128128
resolve(xhr.response.path);
129129
} else {
130130
// Something weird happened.
131-
reject(
132-
new Error(
133-
"Failed to upload file: status " +
134-
xhr.status +
135-
" and response: " +
136-
xhr.response
137-
)
138-
);
131+
132+
if (xhr.response.error) {
133+
// The server sent us a particular message.
134+
reject(new Error(xhr.response.error))
135+
} else {
136+
// The server did not help us. Compose a message.
137+
reject(
138+
new Error(
139+
"Failed to upload file: status " +
140+
xhr.status +
141+
" and response: " +
142+
JSON.stringify(xhr.response)
143+
)
144+
);
145+
}
139146
}
140147
}
141148
};

src/components/HeaderForm.js

+5-1
Original file line numberDiff line numberDiff line change
@@ -912,7 +912,8 @@ class HeaderForm extends Component {
912912
this.setState({ uploadInProgress: val });
913913
};
914914

915-
// Sends uploaded file to server and returns a path to the file
915+
// Sends uploaded file to server and returns a path to the file, or raises an exception if the upload fails or is rejected.
916+
// If the file upload is canceled, returns nothing.
916917
handleFileUpload = async (fileType, file) => {
917918
if (!(this.props.APIInterface instanceof LocalAPI) && file.size > config.MAXUPLOADSIZE) {
918919
this.showFileSizeAlert();
@@ -927,11 +928,14 @@ class HeaderForm extends Component {
927928
// Refresh the graphs right away
928929
this.getMountedFilenames();
929930
}
931+
// TODO: Is only one upload actually ever in progress at a time? Probably
932+
// it's possible to have several!!!
930933
this.setUploadInProgress(false);
931934
return fileName;
932935
} catch (e) {
933936
if (!this.cancelSignal.aborted) {
934937
// Only pass along errors if we haven't canceled our fetches.
938+
this.setUploadInProgress(false);
935939
throw e;
936940
}
937941
}

src/components/TrackFilePicker.js

+19-8
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,9 @@ import { Input } from "reactstrap";
77

88
/*
99
* A selection dropdown component that select files.
10-
* Expects a file type object in the form of {"name": string, "type": string}
10+
* Expects a collection of track objects in the form of {"name": string, "type": string}
1111
*
12-
* The handleInputChange function expects to be passed an option type object in the form of
13-
* {"label": string, "value": file}
12+
* The handleInputChange function expects to be passed a bare value.
1413
*
1514
* See demo and test file for examples of this component.
1615
*/
@@ -30,11 +29,23 @@ export const TrackFilePicker = ({
3029
let acceptedExtensions = config.fileTypeToExtensions[fileType];
3130

3231
async function uploadOnChange() {
33-
const file = uploadFileInput.current.files[0];
34-
35-
const completePath = await handleFileUpload(fileType, file);
36-
console.log("TrackFilePicker got an upload result:", completePath);
37-
handleInputChange(completePath);
32+
// TODO: we have a ref here for a reason, but the ref's value can be null
33+
// when the upload await finishes. So we capture it to a local.
34+
const uploadingInput = uploadFileInput.current;
35+
const file = uploadingInput.files[0];
36+
37+
try {
38+
const completePath = await handleFileUpload(fileType, file);
39+
console.log("TrackFilePicker got an upload result:", completePath);
40+
// This will be nothing if the upoload was canceled.
41+
handleInputChange(completePath);
42+
} catch (e) {
43+
console.error("TrackFilePicker could not upload: ", e);
44+
// TODO: Display error to user in a better way
45+
alert(e.toString());
46+
// Clear out the uploaded file to show it didn't work.
47+
uploadingInput.value = null;
48+
}
3849
}
3950

4051
function mountedOnChange(option) {

src/config.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@
114114
"fileTypeToExtensions": {
115115
"graph": ".xg,.vg,.hg,.gbz,.pg,.db",
116116
"haplotype": ".gbwt,.gbz",
117-
"read": ".gam,.gaf.gz"
117+
"read": ".gam"
118118
},
119119

120120
"MAXUPLOADSIZE": 5242880,

src/server.mjs

+59-16
Original file line numberDiff line numberDiff line change
@@ -321,29 +321,66 @@ api.post("/trackFileSubmission", upload.single("trackFile"), (req, res) => {
321321
}
322322
});
323323

324-
function indexGamSorted(req, res) {
325-
const prefix = req.file.path.substring(0, req.file.path.lastIndexOf("."));
326-
const sortedGamFile = fs.createWriteStream(prefix + ".sorted.gam", {
324+
function indexGamSorted(req, res, next) {
325+
let readsPath = req.file.path;
326+
let prefix;
327+
let sortedSuffix;
328+
let indexSuffix;
329+
if (readsPath.endsWith(".gaf") || readsPath.endsWith(".gaf.gz")) {
330+
throw new BadRequestError(`Server-side sorting and indexing not yet implemented for GAF: ${readsPath}`);
331+
} else if (readsPath.endsWith(".gam")) {
332+
prefix = readsPath.substring(0, req.file.path.lastIndexOf(".gam"));
333+
sortedSuffix = ".sorted.gam";
334+
indexSuffix = ".sorted.gam.gai";
335+
} else {
336+
throw new BadRequestError(`Read file is not a GAF or GAM: ${readsPath}`);
337+
}
338+
339+
const sortedReadsFile = fs.createWriteStream(prefix + sortedSuffix, {
327340
encoding: "binary",
328341
});
329-
const vgIndexChild = spawn(find_vg(), [
342+
343+
let vgGamsortParams = [
330344
"gamsort",
331345
"-i",
332-
prefix + ".sorted.gam.gai",
346+
prefix + indexSuffix,
333347
req.file.path,
334-
]);
348+
];
349+
const vgGamsortChild = spawn(find_vg(), vgGamsortParams);
350+
351+
req.error = Buffer.alloc(0);
352+
353+
let sentResponse = false;
354+
355+
vgGamsortChild.on("error", function (err) {
356+
console.log(
357+
"Error executing " +
358+
find_vg() + " " +
359+
vgGamsortParams.join(" ") +
360+
": " +
361+
err
362+
);
363+
if (!sentResponse) {
364+
sentResponse = true;
365+
return next(new VgExecutionError("vg gamsort failed"));
366+
}
367+
});
335368

336-
vgIndexChild.stderr.on("data", (data) => {
369+
vgGamsortChild.stderr.on("data", (data) => {
337370
console.log(`err data: ${data}`);
371+
req.error += data;
338372
});
339373

340-
vgIndexChild.stdout.on("data", function (data) {
341-
sortedGamFile.write(data);
374+
vgGamsortChild.stdout.on("data", function (data) {
375+
sortedReadsFile.write(data);
342376
});
343377

344-
vgIndexChild.on("close", () => {
345-
sortedGamFile.end();
346-
res.json({ path: path.relative(".", prefix + ".sorted.gam") });
378+
vgGamsortChild.on("close", () => {
379+
sortedReadsFile.end();
380+
if (!sentResponse) {
381+
sentResponse = true;
382+
res.json({ path: path.relative(".", prefix + sortedSuffix) });
383+
}
347384
});
348385
}
349386

@@ -673,20 +710,25 @@ async function getChunkedData(req, res, next) {
673710
let anyGam = false;
674711
let anyGaf = false;
675712
for (const gamFile of gamFiles) {
676-
if (!gamFile.endsWith(".gam") && !gamFile.endsWith(".gaf.gz")) {
677-
throw new BadRequestError("GAM/GAF file doesn't end in .gam or .gaf.gz: " + gamFile);
713+
if (!gamFile.endsWith(".gam") && !gamFile.endsWith(".gaf") && !gamFile.endsWith(".gaf.gz")) {
714+
throw new BadRequestError("GAM/GAF file doesn't end in .gam, .gaf, or .gaf.gz: " + gamFile);
678715
}
679716
if (!isAllowedPath(gamFile)) {
680717
throw new BadRequestError("GAM/GAF file path not allowed: " + gamFile);
681718
}
682719
if (gamFile.endsWith(".gam")) {
683-
// Use a GAM index
720+
// Use a GAM
684721
console.log("pushing gam file", gamFile);
685722
anyGam = true;
686723
}
724+
if (gamFile.endsWith(".gaf")) {
725+
// Use a small GAF without an index
726+
console.log("pushing gaf file", gamFile);
727+
anyGaf = true;
728+
}
687729
if (gamFile.endsWith(".gaf.gz")) {
688730
// Use a GAF with index
689-
console.log("pushing gaf file", gamFile);
731+
console.log("pushing hopefully indexed gaf file", gamFile);
690732
anyGaf = true;
691733
}
692734
vgChunkParams.push("-a", gamFile);
@@ -1615,6 +1657,7 @@ api.get("/getFilenames", (req, res) => {
16151657
if (file.endsWith(".sorted.gam")) {
16161658
result.files.push({ trackFile: clientPath, trackType: "read" });
16171659
}
1660+
// We don't allow un-sorted-and-indexed plain GAF files here
16181661
if (file.endsWith(".gaf.gz")) {
16191662
result.files.push({"trackFile": file, "trackType": "read"});
16201663
}

0 commit comments

Comments
 (0)