Support all file formats for replace/update

1315f0968bbc94aec0b6c3d1d0392cae00130784

Cohee <18619528+Cohee1207@users.noreply.github.com>

3 files changed, +29 -19Showing whitespace changes
public/index.html+1 -1
@@ -4796,7 +4796,7 @@
4796 <input multiple type="file" id="character_import_file" accept=".json, image/png, .yaml, .yml, .charx" name="avatar">4796 <input multiple type="file" id="character_import_file" accept=".json, image/png, .yaml, .yml, .charx" name="avatar">
4797 <input id="character_import_file_type" name="file_type" class="text_pole" maxlength="999" size="2" value="" autocomplete="off">4797 <input id="character_import_file_type" name="file_type" class="text_pole" maxlength="999" size="2" value="" autocomplete="off">
4798 </form>4798 </form>
4799 <input type="file" id="character_replace_file" accept="image/png" name="replace_avatar" hidden>4799 <input type="file" id="character_replace_file" accept=".json, image/png, .yaml, .yml, .charx" name="replace_avatar" hidden>
4800 </div>4800 </div>
4801 <div name="Character List Panel" id="rm_characters_block" class="right_menu">4801 <div name="Character List Panel" id="rm_characters_block" class="right_menu">
4802 <div id="charListFixedTop">4802 <div id="charListFixedTop">
public/script.js+11 -8
@@ -8449,10 +8449,10 @@ async function connectAPISlash(_, text) {
8449/**8449/**
8450 * Imports supported files dropped into the app window.8450 * Imports supported files dropped into the app window.
8451 * @param {File[]} files Array of files to process8451 * @param {File[]} files Array of files to process
8452 * @param {boolean?} preserveFileNames Whether to preserve original file names8452 * @param {Map<File, string>} [data] Extra data to pass to the import function
8453 * @returns {Promise<void>}8453 * @returns {Promise<void>}
8454 */8454 */
8455export async function processDroppedFiles(files, preserveFileNames = false) {8455export async function processDroppedFiles(files, data = new Map()) {
8456 const allowedMimeTypes = [8456 const allowedMimeTypes = [
8457 'application/json',8457 'application/json',
8458 'image/png',8458 'image/png',
@@ -8469,7 +8469,8 @@ export async function processDroppedFiles(files, preserveFileNames = false) {
8469 for (const file of files) {8469 for (const file of files) {
8470 const extension = file.name.split('.').pop().toLowerCase();8470 const extension = file.name.split('.').pop().toLowerCase();
8471 if (allowedMimeTypes.includes(file.type) || allowedExtensions.includes(extension)) {8471 if (allowedMimeTypes.includes(file.type) || allowedExtensions.includes(extension)) {
8472 await importCharacter(file, preserveFileNames);8472 const preservedName = data instanceof Map && data.get(file);
8473 await importCharacter(file, preservedName);
8473 } else {8474 } else {
8474 toastr.warning('Unsupported file type: ' + file.name);8475 toastr.warning('Unsupported file type: ' + file.name);
8475 }8476 }
@@ -8479,10 +8480,10 @@ export async function processDroppedFiles(files, preserveFileNames = false) {
8479/**8480/**
8480 * Imports a character from a file.8481 * Imports a character from a file.
8481 * @param {File} file File to import8482 * @param {File} file File to import
8482 * @param {boolean?} preserveFileName Whether to preserve original file name8483 * @param {string?} preserveFileName Whether to preserve original file name
8483 * @returns {Promise<void>}8484 * @returns {Promise<void>}
8484 */8485 */
8485async function importCharacter(file, preserveFileName = false) {8486async function importCharacter(file, preserveFileName = '') {
8486 if (is_group_generating || is_send_press) {8487 if (is_group_generating || is_send_press) {
8487 toastr.error('Cannot import characters while generating. Stop the request and try again.', 'Import aborted');8488 toastr.error('Cannot import characters while generating. Stop the request and try again.', 'Import aborted');
8488 throw new Error('Cannot import character while generating');8489 throw new Error('Cannot import character while generating');
@@ -8498,7 +8499,7 @@ async function importCharacter(file, preserveFileName = false) {
8498 const formData = new FormData();8499 const formData = new FormData();
8499 formData.append('avatar', file);8500 formData.append('avatar', file);
8500 formData.append('file_type', format);8501 formData.append('file_type', format);
8501 formData.append('preserve_file_name', String(preserveFileName));8502 if (preserveFileName) formData.append('preserved_name', preserveFileName);
85028503
8503 const data = await jQuery.ajax({8504 const data = await jQuery.ajax({
8504 type: 'POST',8505 type: 'POST',
@@ -10637,10 +10638,12 @@ jQuery(async function () {
10637 }10638 }
1063810639
10639 try {10640 try {
10640 const cloneFile = new File([file], characters[this_chid].avatar, { type: file.type });
10641 const chatFile = characters[this_chid]['chat'];10641 const chatFile = characters[this_chid]['chat'];
10642 await processDroppedFiles([cloneFile], true);10642 const data = new Map();
10643 data.set(file, characters[this_chid].avatar);
10644 await processDroppedFiles([file], data);
10643 await openCharacterChat(chatFile);10645 await openCharacterChat(chatFile);
10646 await fetch(getThumbnailUrl('avatar', characters[this_chid].avatar), { cache: 'no-cache' });
10644 } catch {10647 } catch {
10645 toastr.error('Failed to replace the character card.', 'Something went wrong');10648 toastr.error('Failed to replace the character card.', 'Something went wrong');
10646 }10649 }
src/endpoints/characters.js+17 -10
@@ -499,15 +499,16 @@ function convertWorldInfoToCharacterBook(name, entries) {
499 * Import a character from a YAML file.499 * Import a character from a YAML file.
500 * @param {string} uploadPath Path to the uploaded file500 * @param {string} uploadPath Path to the uploaded file
501 * @param {{ request: import('express').Request, response: import('express').Response }} context Express request and response objects501 * @param {{ request: import('express').Request, response: import('express').Response }} context Express request and response objects
502 * @param {string|undefined} preservedFileName Preserved file name
502 * @returns {Promise<string>} Internal name of the character503 * @returns {Promise<string>} Internal name of the character
503 */504 */
504async function importFromYaml(uploadPath, context) {505async function importFromYaml(uploadPath, context, preservedFileName) {
505 const fileText = fs.readFileSync(uploadPath, 'utf8');506 const fileText = fs.readFileSync(uploadPath, 'utf8');
506 fs.rmSync(uploadPath);507 fs.rmSync(uploadPath);
507 const yamlData = yaml.parse(fileText);508 const yamlData = yaml.parse(fileText);
508 console.log('Importing from YAML');509 console.log('Importing from YAML');
509 yamlData.name = sanitize(yamlData.name);510 yamlData.name = sanitize(yamlData.name);
510 const fileName = getPngName(yamlData.name, context.request.user.directories);511 const fileName = preservedFileName || getPngName(yamlData.name, context.request.user.directories);
511 let char = convertToV2({512 let char = convertToV2({
512 'name': yamlData.name,513 'name': yamlData.name,
513 'description': yamlData.context ?? '',514 'description': yamlData.context ?? '',
@@ -532,9 +533,10 @@ async function importFromYaml(uploadPath, context) {
532 * @param {string} uploadPath533 * @param {string} uploadPath
533 * @param {object} params534 * @param {object} params
534 * @param {import('express').Request} params.request535 * @param {import('express').Request} params.request
536 * @param {string|undefined} preservedFileName Preserved file name
535 * @returns {Promise<string>} Internal name of the character537 * @returns {Promise<string>} Internal name of the character
536 */538 */
537async function importFromCharX(uploadPath, { request }) {539async function importFromCharX(uploadPath, { request }, preservedFileName) {
538 const data = fs.readFileSync(uploadPath);540 const data = fs.readFileSync(uploadPath);
539 fs.rmSync(uploadPath);541 fs.rmSync(uploadPath);
540 console.log('Importing from CharX');542 console.log('Importing from CharX');
@@ -567,7 +569,7 @@ async function importFromCharX(uploadPath, { request }) {
567 unsetFavFlag(card);569 unsetFavFlag(card);
568 card['create_date'] = humanizedISO8601DateTime();570 card['create_date'] = humanizedISO8601DateTime();
569 card.name = sanitize(card.name);571 card.name = sanitize(card.name);
570 const fileName = getPngName(card.name, request.user.directories);572 const fileName = preservedFileName || getPngName(card.name, request.user.directories);
571 const result = await writeCharacterData(avatar, JSON.stringify(card), fileName, request);573 const result = await writeCharacterData(avatar, JSON.stringify(card), fileName, request);
572 return result ? fileName : '';574 return result ? fileName : '';
573}575}
@@ -576,9 +578,10 @@ async function importFromCharX(uploadPath, { request }) {
576 * Import a character from a JSON file.578 * Import a character from a JSON file.
577 * @param {string} uploadPath Path to the uploaded file579 * @param {string} uploadPath Path to the uploaded file
578 * @param {{ request: import('express').Request, response: import('express').Response }} context Express request and response objects580 * @param {{ request: import('express').Request, response: import('express').Response }} context Express request and response objects
581 * @param {string|undefined} preservedFileName Preserved file name
579 * @returns {Promise<string>} Internal name of the character582 * @returns {Promise<string>} Internal name of the character
580 */583 */
581async function importFromJson(uploadPath, { request }) {584async function importFromJson(uploadPath, { request }, preservedFileName) {
582 const data = fs.readFileSync(uploadPath, 'utf8');585 const data = fs.readFileSync(uploadPath, 'utf8');
583 fs.unlinkSync(uploadPath);586 fs.unlinkSync(uploadPath);
584587
@@ -590,7 +593,7 @@ async function importFromJson(uploadPath, { request }) {
590 unsetFavFlag(jsonData);593 unsetFavFlag(jsonData);
591 jsonData = readFromV2(jsonData);594 jsonData = readFromV2(jsonData);
592 jsonData['create_date'] = humanizedISO8601DateTime();595 jsonData['create_date'] = humanizedISO8601DateTime();
593 const pngName = getPngName(jsonData.data?.name || jsonData.name, request.user.directories);596 const pngName = preservedFileName || getPngName(jsonData.data?.name || jsonData.name, request.user.directories);
594 const char = JSON.stringify(jsonData);597 const char = JSON.stringify(jsonData);
595 const result = await writeCharacterData(defaultAvatarPath, char, pngName, request);598 const result = await writeCharacterData(defaultAvatarPath, char, pngName, request);
596 return result ? pngName : '';599 return result ? pngName : '';
@@ -600,7 +603,7 @@ async function importFromJson(uploadPath, { request }) {
600 if (jsonData.creator_notes) {603 if (jsonData.creator_notes) {
601 jsonData.creator_notes = jsonData.creator_notes.replace('Creator\'s notes go here.', '');604 jsonData.creator_notes = jsonData.creator_notes.replace('Creator\'s notes go here.', '');
602 }605 }
603 const pngName = getPngName(jsonData.name, request.user.directories);606 const pngName = preservedFileName || getPngName(jsonData.name, request.user.directories);
604 let char = {607 let char = {
605 'name': jsonData.name,608 'name': jsonData.name,
606 'description': jsonData.description ?? '',609 'description': jsonData.description ?? '',
@@ -626,7 +629,7 @@ async function importFromJson(uploadPath, { request }) {
626 if (jsonData.creator_notes) {629 if (jsonData.creator_notes) {
627 jsonData.creator_notes = jsonData.creator_notes.replace('Creator\'s notes go here.', '');630 jsonData.creator_notes = jsonData.creator_notes.replace('Creator\'s notes go here.', '');
628 }631 }
629 const pngName = getPngName(jsonData.char_name, request.user.directories);632 const pngName = preservedFileName || getPngName(jsonData.char_name, request.user.directories);
630 let char = {633 let char = {
631 'name': jsonData.char_name,634 'name': jsonData.char_name,
632 'description': jsonData.char_persona ?? '',635 'description': jsonData.char_persona ?? '',
@@ -1089,8 +1092,8 @@ function getPngName(file, directories) {
1089 * @returns {string | undefined} - The preserved name if the request is valid, otherwise undefined1092 * @returns {string | undefined} - The preserved name if the request is valid, otherwise undefined
1090 */1093 */
1091function getPreservedName(request) {1094function getPreservedName(request) {
1092 return request.body.file_type === 'png' && request.body.preserve_file_name === 'true' && request.file?.originalname1095 return typeof request.body.preserved_name === 'string' && request.body.preserved_name.length > 0
1093 ? path.parse(request.file.originalname).name1096 ? path.parse(request.body.preserved_name).name
1094 : undefined;1097 : undefined;
1095}1098}
10961099
@@ -1123,6 +1126,10 @@ router.post('/import', urlencodedParser, async function (request, response) {
1123 return response.sendStatus(400);1126 return response.sendStatus(400);
1124 }1127 }
11251128
1129 if (preservedFileName) {
1130 invalidateThumbnail(request.user.directories, 'avatar', `${preservedFileName}.png`);
1131 }
1132
1126 response.send({ file_name: fileName });1133 response.send({ file_name: fileName });
1127 } catch (err) {1134 } catch (err) {
1128 console.log(err);1135 console.log(err);