@@ -7,16 +7,19 @@ $(function() {
7
7
) ;
8
8
} ) ;
9
9
10
- // ASYNC UPLOAD LOGIC
10
+ // SEQUENTIAL UPLOAD LOGIC
11
+ // Simulates multiple file upload by breaking file down and submitting sequence of uploads
12
+
13
+ //
11
14
const getPathPrefix = ( ) => {
12
15
const hostname = window . location . hostname
13
16
const subdomain = hostname . split ( '.' ) [ 0 ] ;
14
17
return subdomain === 'localhost' ? '' : '/gids'
15
18
} ;
16
19
const PATH_PREFIX = getPathPrefix ( ) ; ;
17
20
18
- // Open dialog during initial async upload to disable page
19
- $ ( "#async -upload-dialog" ) . dialog ( {
21
+ // Open dialog during sequential upload to disable page
22
+ $ ( "#sequential -upload-dialog" ) . dialog ( {
20
23
autoOpen : false ,
21
24
modal : true ,
22
25
width : "auto" ,
@@ -26,235 +29,91 @@ $(function() {
26
29
}
27
30
} ) ;
28
31
29
- // POLL UPLOAD STATUS
30
- const ON_SCREEN_POLL_RATE = 5_000 ;
31
- const BACKGROUND_POLL_RATE = 10_000 ;
32
-
33
- const capitalize = ( str , titlecase ) => titlecase ? str . charAt ( 0 ) . toUpperCase ( ) + str . slice ( 1 ) : str ;
34
-
35
- // CRUD methods for local storage
36
- const getUploadQueue = ( ) => {
37
- try {
38
- const uploadQueue = JSON . parse ( localStorage . getItem ( "uploadQueue" ) ) ;
39
- if ( Array . isArray ( uploadQueue ) ) {
40
- return uploadQueue ;
41
- }
42
- throw new TypeError ( )
43
- } catch ( error ) {
44
- localStorage . setItem ( "uploadQueue" , "[]" ) ;
45
- return [ ] ;
46
- }
47
- } ;
48
- const addToQueue = ( uploadId ) => {
49
- const uploadQueue = getUploadQueue ( ) ;
50
- uploadQueue . push ( uploadId ) ;
51
- localStorage . setItem ( "uploadQueue" , JSON . stringify ( [ ...new Set ( uploadQueue ) ] ) ) ;
52
- } ;
53
- const removeFromQueue = ( uploadId ) => {
54
- const uploadQueue = getUploadQueue ( ) ;
55
- const filteredQueue = uploadQueue . filter ( ( id ) => id !== uploadId ) ;
56
- localStorage . setItem ( "uploadQueue" , JSON . stringify ( [ ...new Set ( filteredQueue ) ] ) ) ;
57
- } ;
58
-
59
- // Cancel upload if still active
60
- const cancelUpload = async ( icon , uploadId ) => {
61
- $ ( icon ) . off ( "click mouseleave" ) ;
62
- try {
63
- await $ . ajax ( {
64
- url : `${ PATH_PREFIX } /uploads/${ uploadId } /cancel` ,
65
- type : "PATCH" ,
66
- contentType : false ,
67
- processData : false ,
68
- } ) ;
69
- pollUploadStatus ( ) ;
70
- } catch ( error ) {
71
- console . error ( error ) ;
72
- const uploadStatusDiv = $ ( `#async-upload-status-${ uploadId } ` ) [ 0 ] ;
73
- const { titlecase } = uploadStatusDiv . dataset || false ;
74
- $ ( uploadStatusDiv ) . html ( capitalize ( "failed to cancel" , titlecase ) ) ;
75
- await new Promise ( ( resolve ) => setTimeout ( resolve , ON_SCREEN_POLL_RATE ) ) ;
76
- pollUploadStatus ( ) ;
77
- }
78
- } ;
79
-
80
- // Dialog and dialog display method for when async upload complete
81
- $ ( "#async-upload-alert" ) . dialog ( {
82
- autoOpen : false ,
83
- modal : true ,
84
- width : "auto" ,
85
- residable : false ,
86
- open : function ( ) {
87
- const { uploadId, csvType } = $ ( this ) . data ( ) ;
88
- $ ( this ) . html (
89
- `<p>${ csvType } file upload complete</p>` +
90
- '<p>Click ' + `<a href="${ PATH_PREFIX } /uploads/${ uploadId } ">here</a>` +
91
- ' for a more detailed report</p>'
92
- ) ;
93
- }
94
- } ) ;
95
- const displayAlert = ( uploadId , csvType ) => {
96
- $ ( "#async-upload-alert" ) . data ( { "uploadId" : uploadId , "csvType" : csvType } ) . dialog ( "open" ) ;
97
- } ;
98
-
99
- // Grab active client-side uploads from local storage and poll each upload for status
100
- let consecutiveFails = 0 ;
101
- const pollUploadStatus = async ( ) => {
102
- const uploadQueue = getUploadQueue ( ) ;
103
- uploadQueue . forEach ( async ( uploadId ) => {
104
- const uploadStatusDiv = $ ( `#async-upload-status-${ uploadId } ` ) [ 0 ] ;
105
- const onScreen = typeof uploadStatusDiv !== "undefined" ;
106
- const pollRate = onScreen ? ON_SCREEN_POLL_RATE : BACKGROUND_POLL_RATE ;
107
- const { titlecase } = uploadStatusDiv ?. dataset || false ;
108
- try {
109
- const xhr = new XMLHttpRequest ( ) ;
110
- const getUploadStatus = ( ) => {
111
- xhr . open ( "GET" , `${ PATH_PREFIX } /uploads/${ uploadId } /status` ) ;
112
- xhr . send ( ) ;
113
- } ;
114
- xhr . onload = function ( ) {
115
- if ( this . status === 200 ) {
116
- consecutiveFails = 0 ;
117
- const { message, active, ok, canceled, type } = JSON . parse ( xhr . response ) . async_status ;
118
- // If upload active and status currently visible on screen
119
- if ( active ) {
120
- if ( onScreen ) {
121
- // Update DOM
122
- $ ( uploadStatusDiv ) . html (
123
- '<i class="fa fa-gear fa-spin upload-icon" style="font-size:16px"></i>' +
124
- `<div>${ capitalize ( message , titlecase ) } </div>`
125
- ) ;
126
- const icon = $ ( uploadStatusDiv ) . find ( "i" ) ;
127
- // Enable cancel upload button
128
- $ ( icon ) . on ( {
129
- mouseover : function ( _event ) {
130
- clearInterval ( pollingInterval ) ;
131
- $ ( this ) . removeClass ( "fa-gear fa-spin" ) . addClass ( "fa-solid fa-times" ) . css ( { color : "red" , fontSize : "20px" } ) ;
132
- $ ( this ) . on ( "click" , ( _event ) => cancelUpload ( this , uploadId ) ) ;
133
- } ,
134
- mouseleave : function ( _event ) {
135
- pollingInterval = setInterval ( getUploadStatus , pollRate ) ;
136
- $ ( this ) . removeClass ( "fa-solid fa-times" ) . addClass ( "fa-gear fa-spin" ) . css ( { color : "#333" , fontSize : "16px" } ) ;
137
- $ ( this ) . off ( "click" ) ;
138
- }
139
- } ) ;
140
- }
141
- // If upload completed or canceled
142
- } else {
143
- removeFromQueue ( uploadId ) ;
144
- clearInterval ( pollingInterval ) ;
145
- // If upload status currently visible on screen
146
- if ( onScreen ) {
147
- $ ( uploadStatusDiv ) . html ( capitalize ( ok ? "succeeded" : "failed" , titlecase ) ) ;
148
- }
149
- // If on upload#show page, reload page to render flash alerts
150
- if ( window . location . pathname === `${ PATH_PREFIX } /uploads/${ uploadId } ` ) {
151
- window . location . reload ( ) ;
152
- // Otherwise render link to alerts in pop dialog
153
- } else if ( ! canceled ) {
154
- displayAlert ( uploadId , type ) ;
155
- }
156
- }
157
- } else {
158
- consecutiveFails ++ ;
159
- if ( consecutiveFails === 5 ) {
160
- removeFromQueue ( uploadId ) ;
161
- clearInterval ( pollingInterval ) ;
162
- }
163
- }
164
- } ;
165
- getUploadStatus ( ) ;
166
- let pollingInterval = setInterval ( getUploadStatus , pollRate ) ;
167
- } catch ( error ) {
168
- console . error ( error ) ;
169
- }
170
- } ) ;
171
- } ;
172
- $ ( document ) . ready ( ( ) => pollUploadStatus ( ) ) ;
173
-
174
- // Reset active upload if for some reason stuck on "Loading . . ."
175
- // Not sure this is necessary, but technically someone could mess with local storage and
176
- // it would mess up queue
177
- $ ( ".default-async-loading" ) . on ( {
178
- mouseover : function ( _event ) {
179
- $ ( this ) . removeClass ( "fa-gear fa-spin" ) . addClass ( "fa-solid fa-rotate" ) . css ( { color : "green" } ) ;
180
- $ ( this ) . on ( "click" , ( _event ) => {
181
- const { uploadId } = this . dataset ;
182
- addToQueue ( parseInt ( uploadId ) ) ;
183
- pollUploadStatus ( ) ;
184
- } ) ;
185
- } ,
186
- mouseleave : function ( _event ) {
187
- $ ( this ) . removeClass ( "fa-solid fa-rotate" ) . addClass ( "fa-gear fa-spin" ) . css ( { color : "#333" , fontSize : "16px" } ) ;
188
- $ ( this ) . off ( "click" ) ;
189
- }
190
- } ) ;
191
-
192
- // ASYNC SUBMIT ACTION
193
- // Submit logic for new upload form when async upload enabled
194
- $ ( "#async-submit-btn" ) . on ( "click" , async function ( event ) {
32
+ // Submit logic for new upload form when sequential upload enabled
33
+ $ ( "#seq-submit-btn" ) . on ( "click" , async function ( event ) {
195
34
event . preventDefault ( ) ;
196
- // Grab form data and validate file extension
35
+ // Grab form data and validate file selected
197
36
const form = $ ( "#new_upload" ) [ 0 ] ;
198
- const formData = new FormData ( form ) ;
199
- const file = formData . get ( "upload[upload_file]" ) ;
200
- const ext = file . name !== '' ? file . name . slice ( file . name . lastIndexOf ( "." ) ) : null ;
201
- const fileInput = $ ( "#upload_upload_file" ) ;
202
- const validExts = $ ( fileInput ) . attr ( "accept" ) . split ( ", " ) ;
203
- $ ( fileInput ) [ 0 ] . setCustomValidity ( '' ) ;
204
- if ( ext !== null && ! validExts . includes ( ext ) ) {
205
- $ ( fileInput ) [ 0 ] . setCustomValidity ( `${ ext } is not a valid file format.` ) ;
206
- }
207
37
if ( ! form . reportValidity ( ) ) {
208
38
return ;
209
39
}
40
+ const formData = new FormData ( form ) ;
41
+ const file = formData . get ( "upload[upload_file]" ) ;
42
+
210
43
// Open dialog and disable page until client-side processing complete
211
- $ ( "#async -upload-dialog" ) . dialog ( "open" ) ;
44
+ $ ( "#sequential -upload-dialog" ) . dialog ( "open" ) ;
212
45
$ ( this ) . html (
213
- '<div id="async -submit-btn-div">' +
46
+ '<div id="sequential -submit-btn-div">' +
214
47
'<i class="fa fa-gear fa-spin" style="font-size:16px"></i>' +
215
48
'Submitting . . .' +
216
49
'</div>'
217
50
) ;
218
- const csvType = formData . get ( "upload[csv_type]" ) ;
219
- let uploadId = null ;
51
+
220
52
// Divide upload file into smaller files
221
53
const blobs = [ ] ;
222
54
const generateBlobs = async ( ) => {
223
55
const chunkSize = parseInt ( this . dataset . chunkSize ) ;
56
+ const text = await file . text ( ) ;
57
+ const header = text . slice ( 0 , text . indexOf ( '\n' ) + 1 ) ;
224
58
for ( let start = 0 ; start < file . size ; start += chunkSize ) {
225
- const blob = file . slice ( start , start + chunkSize , "text/plain" ) ;
226
- blobs . push ( blob ) ;
59
+ let end = start + chunkSize ;
60
+ let charsToEndOfRow = 0 ;
61
+ // Ensure rows not divided between blobs
62
+ if ( text [ end - 1 ] !== "\n" ) {
63
+ charsToEndOfRow = text . slice ( end ) . indexOf ( "\n" ) + 1 ;
64
+ end += charsToEndOfRow ;
65
+ } ;
66
+ const blob = file . slice ( start , end , "text/plain" ) ;
67
+ // Add header if not already present
68
+ const fileBits = start === 0 ? [ blob ] : [ header , blob ]
69
+ const newFile = new File ( fileBits , { type : "text/plain" } ) ;
70
+ blobs . push ( newFile ) ;
71
+ start += charsToEndOfRow ;
227
72
}
228
73
} ;
74
+
229
75
// Send individual POST request for each blob, simulating multiple file upload
76
+ let uploadId ;
230
77
const submitBlobs = async ( ) => {
78
+ const total = blobs . length ;
79
+ const idx = file . name . lastIndexOf ( "." ) ;
80
+ const [ basename , ext ] = [ file . name . slice ( 0 , idx ) , file . name . slice ( idx ) ] ;
231
81
try {
232
82
for ( let i = 0 ; i < blobs . length ; i ++ ) {
233
- formData . set ( "upload[upload_file]" , blobs [ i ] , file . name ) ;
83
+ const current = i + 1 ;
84
+ const filePosition = `${ current . toString ( ) . padStart ( 2 , '0' ) } _of_${ total . toString ( ) . padStart ( 2 , '0' ) } ` ;
85
+ const fileName = `${ basename } _${ filePosition } ${ ext } ` ;
86
+ formData . set ( "upload[upload_file]" , blobs [ i ] , fileName ) ;
87
+ // Set multiple_file_upload to true after first upload
88
+ if ( i > 0 ) {
89
+ formData . set ( "upload[multiple_file_upload]" , true ) ;
90
+ }
234
91
// Include metadata in payload to track upload progress across multiple requests
235
- formData . set ( "upload[metadata][upload_id]" , uploadId ) ;
236
- formData . set ( "upload[metadata][count][current]" , i + 1 ) ;
237
- formData . set ( "upload[metadata][count][total]" , blobs . length ) ;
238
- const response = await $ . ajax ( {
239
- url : `${ PATH_PREFIX } /uploads` ,
240
- type : "POST" ,
241
- data : formData ,
242
- dataType : "json" ,
243
- contentType : false ,
244
- processData : false ,
92
+ formData . set ( "upload[sequence][current]" , current ) ;
93
+ formData . set ( "upload[sequence][total]" , total ) ;
94
+ const response = await fetch ( `${ PATH_PREFIX } /uploads` , {
95
+ method : "POST" ,
96
+ body : formData
245
97
} ) ;
246
- uploadId = response . id ;
98
+
99
+ if ( ! response . ok ) {
100
+ throw new Error ( `Upload failed with status ${ response . status } ` ) ;
101
+ }
102
+
103
+ const data = await response . json ( ) ;
104
+ if ( data . final ) {
105
+ uploadId = data . upload . id ;
106
+ }
247
107
}
248
- // If successful, save upload ID in local storage to enable status polling
249
- addToQueue ( uploadId ) ;
250
108
window . location . href = `${ PATH_PREFIX } /uploads/${ uploadId } ` ;
251
109
} catch ( error ) {
252
110
console . error ( error ) ;
111
+ const csvType = formData . get ( "upload[csv_type]" ) ;
253
112
window . location . href = `${ PATH_PREFIX } /uploads/new/${ csvType } ` ;
254
113
}
255
114
} ;
256
115
257
- generateBlobs ( ) ;
116
+ await generateBlobs ( ) ;
258
117
submitBlobs ( ) ;
259
118
} ) ;
260
119
} ) ;
0 commit comments