-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathcomplete_upload.ts
More file actions
110 lines (102 loc) · 3.79 KB
/
Copy pathcomplete_upload.ts
File metadata and controls
110 lines (102 loc) · 3.79 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
import { MapValidity } from 'schema/maps';
import { actionError } from 'services/helpers';
import { submitErrorMap } from 'services/maps/maps_repo';
import { getServerContext } from 'services/server_context';
import { getUserSession } from 'services/session/session';
/**
* Validates and publishes a map whose archive has already been uploaded to S3 (a fresh upload or a
* reupload), making it public when valid. A freshly-created map is rolled back on failure; a
* reupload reverts to its previous valid state.
*
* Invoked by the POST /api/maps/submit/complete route. Also called in-process by the integration
* tests: the in-memory fake S3 only round-trips within a single process, so they seed the archive
* and run this directly rather than over HTTP.
*/
export async function completeMapUpload(id: string, isReupload: boolean) {
const { mapsRepo, s3Handler } = await getServerContext();
const validityResult = await mapsRepo.setValidity(
id,
isReupload ? MapValidity.REUPLOADED : MapValidity.UPLOADED
);
if (!validityResult.success) {
return actionError({
errorBody: {},
message: 'Could not update map upload status.',
resultError: validityResult,
shouldLog: true,
});
}
const dbMapResult = await mapsRepo.getMap(id);
async function cleanupFailedUpload() {
// A fresh upload has no prior published state, so delete the placeholder record; a failed
// reupload keeps the existing map and reverts it to valid. (getMap can't tell them apart - it
// only returns public maps, and a fresh upload is still hidden here - so key off `isReupload`.)
if (!isReupload) {
// Mark as invalid first in case any other parts of deletion fail
await mapsRepo.setValidity(id, MapValidity.INVALID);
await mapsRepo.deleteMap({ id });
} else {
await s3Handler.deleteFiles(id, true);
await mapsRepo.setValidity(id, MapValidity.VALID);
}
}
const session = await getUserSession();
if (!session) {
await cleanupFailedUpload();
return actionError({
errorBody: {},
message: 'You must be logged in to submit maps.',
});
}
if (dbMapResult.success && dbMapResult.value.uploader !== session.id) {
await cleanupFailedUpload();
if (!isReupload) {
return actionError({
errorBody: {},
message: 'You must begin and complete the upload while logged into the same user session.',
});
} else {
return actionError({
errorBody: {},
message: 'Only the original map uploader can reupload a map.',
});
}
}
// Open it for reading and begin processing
const openMapResult = await s3Handler.openMapFile(id, true);
if (!openMapResult.success) {
await cleanupFailedUpload();
return actionError({
errorBody: {},
message: 'The file could not be processed.',
resultError: openMapResult,
shouldLog: true,
});
}
const archive = openMapResult.value;
if (archive.size > 1024 * 1024 * 100) {
await cleanupFailedUpload();
// 100MiB. We use MiB because that's what Windows displays in Explorer and therefore what users will expect.
return actionError({
message: 'File is over the filesize limit (100MB)',
errorBody: {},
});
}
const processMapResult = await mapsRepo.validateUploadedMap({
id,
archive,
uploader: session.id,
});
if (!processMapResult.success) {
await cleanupFailedUpload();
// TODO: report all errors back to the client and not just the first one
const [_statusCode, message] = submitErrorMap[processMapResult.errors[0].type];
return actionError({
message: processMapResult.errors[0].userMessage || message,
errorBody: {},
resultError: processMapResult,
shouldLog: true,
});
}
return { success: true, value: processMapResult.value } as const;
}