@teselagen/file-utils 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -0
- package/index.js +297 -0
- package/package.json +13 -0
package/README.md
ADDED
package/index.js
ADDED
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
// packages/file-utils/src/lib/file-utils.js
|
|
2
|
+
import { camelCase, flatMap, remove, startsWith, snakeCase } from "lodash";
|
|
3
|
+
import { loadAsync } from "jszip";
|
|
4
|
+
import Promise2 from "bluebird";
|
|
5
|
+
import { parse, unparse } from "papaparse";
|
|
6
|
+
var logDebug = (...args) => {
|
|
7
|
+
if (process.env.DEBUG_CSV_PARSING) {
|
|
8
|
+
console.log(...args);
|
|
9
|
+
}
|
|
10
|
+
};
|
|
11
|
+
var allowedCsvFileTypes = [".csv", ".txt", ".xlsx"];
|
|
12
|
+
var isZipFile = (file) => {
|
|
13
|
+
const type = file.mimetype || file.type;
|
|
14
|
+
return type === "application/zip" || type === "application/x-zip-compressed";
|
|
15
|
+
};
|
|
16
|
+
var getExt = (file) => file.name.split(".").pop();
|
|
17
|
+
var isExcelFile = (file) => getExt(file) === "xlsx";
|
|
18
|
+
var isCsvFile = (file) => getExt(file) === "csv";
|
|
19
|
+
var isTextFile = (file) => ["text", "txt"].includes(getExt(file));
|
|
20
|
+
var isCsvOrExcelFile = (file) => isCsvFile(file) || isExcelFile(file);
|
|
21
|
+
var extractZipFiles = async (allFiles) => {
|
|
22
|
+
if (!Array.isArray(allFiles))
|
|
23
|
+
allFiles = [allFiles];
|
|
24
|
+
allFiles = [...allFiles];
|
|
25
|
+
const zipFiles = remove(allFiles, isZipFile);
|
|
26
|
+
if (!zipFiles.length)
|
|
27
|
+
return allFiles;
|
|
28
|
+
const zipFilesArray = Array.isArray(zipFiles) ? zipFiles : [zipFiles];
|
|
29
|
+
const parsedZips = await Promise2.map(
|
|
30
|
+
zipFilesArray,
|
|
31
|
+
(file) => loadAsync(file instanceof Blob ? file : file.originFileObj)
|
|
32
|
+
);
|
|
33
|
+
const zippedFiles = flatMap(
|
|
34
|
+
parsedZips,
|
|
35
|
+
(zip) => Object.keys(zip.files).map((key) => zip.files[key])
|
|
36
|
+
);
|
|
37
|
+
const unzippedFiles = await Promise2.map(zippedFiles, (file) => {
|
|
38
|
+
return file.async("blob").then(function(fileData) {
|
|
39
|
+
const newFileObj = new File([fileData], file.name);
|
|
40
|
+
return {
|
|
41
|
+
name: file.name,
|
|
42
|
+
originFileObj: newFileObj,
|
|
43
|
+
originalFileObj: newFileObj
|
|
44
|
+
};
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
if (unzippedFiles.length) {
|
|
48
|
+
return allFiles.concat(
|
|
49
|
+
unzippedFiles.filter(
|
|
50
|
+
({ name, originFileObj }) => !name.includes("__MACOSX") && !name.includes(".DS_Store") && originFileObj.size !== 0
|
|
51
|
+
)
|
|
52
|
+
);
|
|
53
|
+
} else {
|
|
54
|
+
return allFiles;
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
var defaultCsvParserOptions = {
|
|
58
|
+
header: true,
|
|
59
|
+
skipEmptyLines: "greedy",
|
|
60
|
+
trimHeaders: true
|
|
61
|
+
};
|
|
62
|
+
var setupCsvParserOptions = (parserOptions = {}) => {
|
|
63
|
+
const {
|
|
64
|
+
camelCaseHeaders = false,
|
|
65
|
+
lowerCaseHeaders = false,
|
|
66
|
+
...rest
|
|
67
|
+
} = parserOptions;
|
|
68
|
+
const papaParseOpts = { ...rest };
|
|
69
|
+
if (camelCaseHeaders) {
|
|
70
|
+
logDebug("[CSV-PARSER] camelCasing headers");
|
|
71
|
+
papaParseOpts.transformHeader = (header) => {
|
|
72
|
+
let transHeader = header;
|
|
73
|
+
if (!startsWith(header.trim(), "ext-")) {
|
|
74
|
+
transHeader = camelCase(header);
|
|
75
|
+
}
|
|
76
|
+
if (transHeader) {
|
|
77
|
+
logDebug(
|
|
78
|
+
`[CSV-PARSER] Transformed header from: ${header} to: ${transHeader}`
|
|
79
|
+
);
|
|
80
|
+
transHeader = transHeader.trim();
|
|
81
|
+
} else {
|
|
82
|
+
logDebug(`[CSV-PARSER] Not transforming header: ${header}`);
|
|
83
|
+
}
|
|
84
|
+
return transHeader;
|
|
85
|
+
};
|
|
86
|
+
} else if (lowerCaseHeaders) {
|
|
87
|
+
papaParseOpts.transformHeader = (header) => {
|
|
88
|
+
let transHeader = header;
|
|
89
|
+
if (!startsWith(header, "ext-")) {
|
|
90
|
+
transHeader = header.toLowerCase();
|
|
91
|
+
}
|
|
92
|
+
if (transHeader) {
|
|
93
|
+
logDebug(
|
|
94
|
+
`[CSV-PARSER] Transformed header from: ${header} to: ${transHeader}`
|
|
95
|
+
);
|
|
96
|
+
transHeader = transHeader.trim();
|
|
97
|
+
} else {
|
|
98
|
+
logDebug(`[CSV-PARSER] Not transforming header: ${header}`);
|
|
99
|
+
}
|
|
100
|
+
return transHeader;
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
return papaParseOpts;
|
|
104
|
+
};
|
|
105
|
+
var normalizeCsvHeaderHelper = (h) => snakeCase(h.toUpperCase()).toUpperCase();
|
|
106
|
+
function normalizeCsvHeader(header) {
|
|
107
|
+
if (header.startsWith("ext-") || header.startsWith("EXT-")) {
|
|
108
|
+
return header;
|
|
109
|
+
}
|
|
110
|
+
return normalizeCsvHeaderHelper(header);
|
|
111
|
+
}
|
|
112
|
+
var parseCsvFile = (csvFile, parserOptions = {}) => {
|
|
113
|
+
return new Promise2((resolve, reject) => {
|
|
114
|
+
const opts = {
|
|
115
|
+
...defaultCsvParserOptions,
|
|
116
|
+
...setupCsvParserOptions(parserOptions),
|
|
117
|
+
complete: (results) => {
|
|
118
|
+
if (results && results.errors && results.errors.length) {
|
|
119
|
+
return reject("Error in csv: " + JSON.stringify(results.errors));
|
|
120
|
+
}
|
|
121
|
+
resolve(results);
|
|
122
|
+
},
|
|
123
|
+
error: (error) => {
|
|
124
|
+
reject(error);
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
logDebug(`[CSV-PARSER] parseCsvFile opts:`, opts);
|
|
128
|
+
parse(csvFile.originFileObj, opts);
|
|
129
|
+
});
|
|
130
|
+
};
|
|
131
|
+
var jsonToCsv = (jsonData, options = {}) => {
|
|
132
|
+
const csv = unparse(jsonData, options);
|
|
133
|
+
return csv;
|
|
134
|
+
};
|
|
135
|
+
var parseCsvString = (csvString, parserOptions = {}) => {
|
|
136
|
+
const opts = {
|
|
137
|
+
...defaultCsvParserOptions,
|
|
138
|
+
...setupCsvParserOptions(parserOptions)
|
|
139
|
+
};
|
|
140
|
+
logDebug(`[CSV-PARSER] parseCsvString opts:`, opts);
|
|
141
|
+
return parse(csvString, opts);
|
|
142
|
+
};
|
|
143
|
+
async function parseCsvOrExcelFile(fileOrFiles, { csvParserOptions } = {}) {
|
|
144
|
+
let csvFile, excelFile, txtFile;
|
|
145
|
+
if (Array.isArray(fileOrFiles)) {
|
|
146
|
+
csvFile = fileOrFiles.find(isCsvFile);
|
|
147
|
+
excelFile = fileOrFiles.find(isExcelFile);
|
|
148
|
+
txtFile = fileOrFiles.find(isTextFile);
|
|
149
|
+
} else {
|
|
150
|
+
if (isExcelFile(fileOrFiles))
|
|
151
|
+
excelFile = fileOrFiles;
|
|
152
|
+
else if (isCsvFile(fileOrFiles))
|
|
153
|
+
csvFile = fileOrFiles;
|
|
154
|
+
else if (isTextFile(fileOrFiles))
|
|
155
|
+
txtFile = fileOrFiles;
|
|
156
|
+
}
|
|
157
|
+
if (!csvFile && !excelFile && !txtFile) {
|
|
158
|
+
throw new Error("No csv or excel files found");
|
|
159
|
+
}
|
|
160
|
+
if (!csvFile && !excelFile)
|
|
161
|
+
csvFile = txtFile;
|
|
162
|
+
if (!csvFile && excelFile && window.parseExcelToCsv) {
|
|
163
|
+
csvFile = await window.parseExcelToCsv(
|
|
164
|
+
excelFile.originFileObj || excelFile
|
|
165
|
+
);
|
|
166
|
+
if (csvFile.error) {
|
|
167
|
+
throw new Error(csvFile.error);
|
|
168
|
+
}
|
|
169
|
+
} else if (excelFile) {
|
|
170
|
+
throw new Error("Excel Parser not initialized on the window");
|
|
171
|
+
}
|
|
172
|
+
const parsedCsv = await parseCsvFile(csvFile, csvParserOptions);
|
|
173
|
+
parsedCsv.originalFile = csvFile;
|
|
174
|
+
return parsedCsv;
|
|
175
|
+
}
|
|
176
|
+
var validateCSVRequiredHeaders = (fields, requiredHeaders, filename) => {
|
|
177
|
+
const missingRequiredHeaders = requiredHeaders.filter((field) => {
|
|
178
|
+
return !fields.includes(field);
|
|
179
|
+
});
|
|
180
|
+
if (missingRequiredHeaders.length) {
|
|
181
|
+
const name = filename ? `The file ${filename}` : "CSV file";
|
|
182
|
+
return `${name} is missing required headers. (${missingRequiredHeaders.join(
|
|
183
|
+
", "
|
|
184
|
+
)})`;
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
var validateCSVRow = (row, requiredHeaders, index) => {
|
|
188
|
+
const missingRequiredFields = requiredHeaders.filter((field) => !row[field]);
|
|
189
|
+
if (missingRequiredFields.length) {
|
|
190
|
+
if (missingRequiredFields.length === 1) {
|
|
191
|
+
return `Row ${index + 1} is missing the required field "${missingRequiredFields[0]}"`;
|
|
192
|
+
} else {
|
|
193
|
+
return `Row ${index + 1} is missing these required fields: ${missingRequiredFields.join(
|
|
194
|
+
", "
|
|
195
|
+
)}`;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
var cleanCommaSeparatedCell = (cellData) => (cellData || "").split(",").map((n) => n.trim()).filter((n) => n);
|
|
200
|
+
var cleanCsvExport = (rows) => {
|
|
201
|
+
const allHeaders = [];
|
|
202
|
+
rows.forEach((row) => {
|
|
203
|
+
Object.keys(row).forEach((header) => {
|
|
204
|
+
if (!allHeaders.includes(header)) {
|
|
205
|
+
allHeaders.push(header);
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
rows.forEach((row) => {
|
|
210
|
+
allHeaders.forEach((header) => {
|
|
211
|
+
row[header] = row[header] || "";
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
return rows;
|
|
215
|
+
};
|
|
216
|
+
var filterFilesInZip = async (file, accepted) => {
|
|
217
|
+
const zipExtracted = await extractZipFiles(file);
|
|
218
|
+
const acceptedFiles = [];
|
|
219
|
+
for (const extFile of zipExtracted) {
|
|
220
|
+
const extension = "." + getExt(extFile);
|
|
221
|
+
if (accepted.some((ext) => ext === extension)) {
|
|
222
|
+
acceptedFiles.push(extFile);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
if (acceptedFiles.length && acceptedFiles.length < zipExtracted.length)
|
|
226
|
+
window.toastr.warning("Some files don't have the proper file extension.");
|
|
227
|
+
if (!acceptedFiles.length)
|
|
228
|
+
window.toastr.warning("No files with the proper extension were found.");
|
|
229
|
+
return acceptedFiles;
|
|
230
|
+
};
|
|
231
|
+
function removeExt(filename) {
|
|
232
|
+
if (filename && filename.includes(".")) {
|
|
233
|
+
return filename.split(".").slice(0, -1).join(".");
|
|
234
|
+
} else {
|
|
235
|
+
return filename;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
async function uploadAndProcessFiles(files = []) {
|
|
239
|
+
if (!files.length)
|
|
240
|
+
return null;
|
|
241
|
+
const formData = new FormData();
|
|
242
|
+
files.forEach(({ originFileObj }) => formData.append("file", originFileObj));
|
|
243
|
+
const response = await window.api.post("/user_uploads/", formData);
|
|
244
|
+
return response.data.map((d) => ({
|
|
245
|
+
encoding: d.encoding,
|
|
246
|
+
mimetype: d.mimetype,
|
|
247
|
+
originalname: d.originalname,
|
|
248
|
+
path: d.path,
|
|
249
|
+
size: d.size
|
|
250
|
+
}));
|
|
251
|
+
}
|
|
252
|
+
async function encodeFilesForRequest(files) {
|
|
253
|
+
const encodedFiles = [];
|
|
254
|
+
for (const file of files) {
|
|
255
|
+
const encoded = await fileToBase64(file.originalFileObj);
|
|
256
|
+
const data = encoded.split(",");
|
|
257
|
+
encodedFiles.push({
|
|
258
|
+
type: file.type,
|
|
259
|
+
base64Data: data[1],
|
|
260
|
+
name: file.name
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
return encodedFiles;
|
|
264
|
+
}
|
|
265
|
+
var fileToBase64 = (file) => {
|
|
266
|
+
return new Promise2((resolve) => {
|
|
267
|
+
const reader = new FileReader();
|
|
268
|
+
reader.onload = function(event) {
|
|
269
|
+
resolve(event.target.result);
|
|
270
|
+
};
|
|
271
|
+
reader.readAsDataURL(file);
|
|
272
|
+
});
|
|
273
|
+
};
|
|
274
|
+
export {
|
|
275
|
+
allowedCsvFileTypes,
|
|
276
|
+
cleanCommaSeparatedCell,
|
|
277
|
+
cleanCsvExport,
|
|
278
|
+
encodeFilesForRequest,
|
|
279
|
+
extractZipFiles,
|
|
280
|
+
filterFilesInZip,
|
|
281
|
+
getExt,
|
|
282
|
+
isCsvFile,
|
|
283
|
+
isCsvOrExcelFile,
|
|
284
|
+
isExcelFile,
|
|
285
|
+
isTextFile,
|
|
286
|
+
isZipFile,
|
|
287
|
+
jsonToCsv,
|
|
288
|
+
normalizeCsvHeader,
|
|
289
|
+
parseCsvFile,
|
|
290
|
+
parseCsvOrExcelFile,
|
|
291
|
+
parseCsvString,
|
|
292
|
+
removeExt,
|
|
293
|
+
setupCsvParserOptions,
|
|
294
|
+
uploadAndProcessFiles,
|
|
295
|
+
validateCSVRequiredHeaders,
|
|
296
|
+
validateCSVRow
|
|
297
|
+
};
|
package/package.json
ADDED