1
0
mirror of https://github.com/laurent22/joplin.git synced 2024-11-24 08:12:24 +02:00

Desktop: Resolves #9693: Allow importing a directory of ENEX files

This commit is contained in:
Laurent Cozic 2024-01-09 22:03:34 +00:00
parent 61a3962eda
commit b2109dab99
9 changed files with 90 additions and 34 deletions

View File

@ -5,6 +5,7 @@ const { cliUtils } = require('./cli-utils.js');
const { app } = require('./app.js'); const { app } = require('./app.js');
import { _ } from '@joplin/lib/locale'; import { _ } from '@joplin/lib/locale';
import { ImportOptions } from '@joplin/lib/services/interop/types'; import { ImportOptions } from '@joplin/lib/services/interop/types';
import { unique } from '@joplin/lib/array';
class Command extends BaseCommand { class Command extends BaseCommand {
public override usage() { public override usage() {
@ -23,7 +24,7 @@ class Command extends BaseCommand {
.map(m => m.format); .map(m => m.format);
return [ return [
['--format <format>', _('Source format: %s', ['auto'].concat(formats).join(', '))], ['--format <format>', _('Source format: %s', ['auto'].concat(unique(formats)).join(', '))],
['-f, --force', _('Do not ask for confirmation.')], ['-f, --force', _('Do not ask for confirmation.')],
['--output-format <output-format>', _('Output format: %s', 'md, html')], ['--output-format <output-format>', _('Output format: %s', 'md, html')],
]; ];

View File

@ -84,6 +84,23 @@ export default class InteropService {
isDefault: true, isDefault: true,
}, dynamicRequireModuleFactory('./InteropService_Importer_EnexToMd')), }, dynamicRequireModuleFactory('./InteropService_Importer_EnexToMd')),
makeImportModule({
format: 'enex',
fileExtensions: ['enex'],
sources: [FileSystemItem.Directory],
description: _('Evernote Export Files (Directory, as HTML)'),
supportsMobile: false,
outputFormat: ImportModuleOutputFormat.Html,
}, dynamicRequireModuleFactory('./InteropService_Importer_EnexToHtml')),
makeImportModule({
format: 'enex',
fileExtensions: ['enex'],
sources: [FileSystemItem.Directory],
description: _('Evernote Export Files (Directory, as Markdown)'),
supportsMobile: false,
}, dynamicRequireModuleFactory('./InteropService_Importer_EnexToMd')),
makeImportModule({ makeImportModule({
format: 'html', format: 'html',
fileExtensions: ['html'], fileExtensions: ['html'],

View File

@ -2,18 +2,20 @@
import Setting from '../../models/Setting'; import Setting from '../../models/Setting';
import shim from '../../shim'; import shim from '../../shim';
import { type ExportMetadata } from './Module';
import { ExportOptions } from './types';
export default class InteropService_Exporter_Base { export default class InteropService_Exporter_Base {
private context_: any = {}; private context_: any = {};
private metadata_: any = {}; private metadata_: ExportMetadata = null;
public async init(_destDir: string, _options: any = {}) {} public async init(_destDir: string, _options: ExportOptions = {}) {}
public async prepareForProcessingItemType(_itemType: number, _itemsToExport: any[]) {} public async prepareForProcessingItemType(_itemType: number, _itemsToExport: any[]) {}
public async processItem(_itemType: number, _item: any) {} public async processItem(_itemType: number, _item: any) {}
public async processResource(_resource: any, _filePath: string) {} public async processResource(_resource: any, _filePath: string) {}
public async close() {} public async close() {}
public setMetadata(md: any) { public setMetadata(md: ExportMetadata) {
this.metadata_ = md; this.metadata_ = md;
} }

View File

@ -1,15 +1,16 @@
/* eslint @typescript-eslint/no-unused-vars: 0, no-unused-vars: 0 */ /* eslint @typescript-eslint/no-unused-vars: 0, no-unused-vars: 0 */
import { ImportExportResult } from './types'; import { ImportExportResult, ImportOptions } from './types';
import Setting from '../../models/Setting'; import Setting from '../../models/Setting';
import shim from '../../shim'; import shim from '../../shim';
import { type ImportMetadata } from './Module';
export default class InteropService_Importer_Base { export default class InteropService_Importer_Base {
private metadata_: any = null; private metadata_: ImportMetadata = null;
protected sourcePath_ = ''; protected sourcePath_ = '';
protected options_: any = {}; protected options_: ImportOptions = {};
public setMetadata(md: any) { public setMetadata(md: any) {
this.metadata_ = md; this.metadata_ = md;

View File

@ -1,20 +1,15 @@
import { ImportExportResult } from './types'; import { ImportExportResult } from './types';
import InteropService_Importer_Base from './InteropService_Importer_Base'; import InteropService_Importer_Base from './InteropService_Importer_Base';
import Folder from '../../models/Folder'; import { enexImporterExec } from './InteropService_Importer_EnexToMd';
import importEnex from '../../import-enex';
const { filename } = require('../../path-utils');
export default class InteropService_Importer_EnexToHtml extends InteropService_Importer_Base { export default class InteropService_Importer_EnexToHtml extends InteropService_Importer_Base {
public async exec(result: ImportExportResult): Promise<ImportExportResult> { public async exec(result: ImportExportResult): Promise<ImportExportResult> {
let folder = this.options_.destinationFolder; return enexImporterExec(
result,
if (!folder) { this.options_.destinationFolder,
const folderTitle = await Folder.findUniqueItemTitle(filename(this.sourcePath_)); this.sourcePath_,
folder = await Folder.save({ title: folderTitle }); this.metadata().fileExtensions,
} { ...this.options_, outputFormat: 'html' },
);
await importEnex(folder.id, this.sourcePath_, { ...this.options_, outputFormat: 'html' });
return result;
} }
} }

View File

@ -1,20 +1,52 @@
import { ImportExportResult } from './types'; import { ImportExportResult, ImportOptions } from './types';
import importEnex from '../../import-enex'; import importEnex from '../../import-enex';
import InteropService_Importer_Base from './InteropService_Importer_Base'; import InteropService_Importer_Base from './InteropService_Importer_Base';
import Folder from '../../models/Folder'; import Folder from '../../models/Folder';
import { FolderEntity } from '../database/types';
import { fileExtension, rtrimSlashes } from '../../path-utils';
import shim from '../../shim';
const { filename } = require('../../path-utils'); const { filename } = require('../../path-utils');
const doImportEnex = async (destFolder: FolderEntity, sourcePath: string, options: ImportOptions) => {
if (!destFolder) {
const folderTitle = await Folder.findUniqueItemTitle(filename(sourcePath));
destFolder = await Folder.save({ title: folderTitle });
}
await importEnex(destFolder.id, sourcePath, options);
};
export const enexImporterExec = async (result: ImportExportResult, destinationFolder: FolderEntity, sourcePath: string, fileExtensions: string[], options: any) => {
sourcePath = rtrimSlashes(sourcePath);
if (await shim.fsDriver().isDirectory(sourcePath)) {
const stats = await shim.fsDriver().readDirStats(sourcePath);
for (const stat of stats) {
const fullPath = `${sourcePath}/${stat.path}`;
if (!fileExtensions.includes(fileExtension(fullPath).toLowerCase())) continue;
try {
await doImportEnex(null, fullPath, options);
} catch (error) {
result.warnings.push(`When importing "${fullPath}": ${error.message}`);
}
}
} else {
await doImportEnex(destinationFolder, sourcePath, options);
}
return result;
};
export default class InteropService_Importer_EnexToMd extends InteropService_Importer_Base { export default class InteropService_Importer_EnexToMd extends InteropService_Importer_Base {
public async exec(result: ImportExportResult) { public async exec(result: ImportExportResult) {
let folder = this.options_.destinationFolder; return enexImporterExec(
result,
if (!folder) { this.options_.destinationFolder,
const folderTitle = await Folder.findUniqueItemTitle(filename(this.sourcePath_)); this.sourcePath_,
folder = await Folder.save({ title: folderTitle }); this.metadata().fileExtensions,
} this.options_,
);
await importEnex(folder.id, this.sourcePath_, this.options_);
return result;
} }
} }

View File

@ -23,7 +23,7 @@ interface BaseMetadata {
isNoteArchive: boolean; isNoteArchive: boolean;
} }
interface ImportMetadata extends BaseMetadata { export interface ImportMetadata extends BaseMetadata {
type: ModuleType.Importer; type: ModuleType.Importer;
format: string; format: string;
@ -35,7 +35,7 @@ export interface ImportModule extends ImportMetadata {
factory(options?: ImportOptions): InteropService_Importer_Base; factory(options?: ImportOptions): InteropService_Importer_Base;
} }
interface ExportMetadata extends BaseMetadata { export interface ExportMetadata extends BaseMetadata {
type: ModuleType.Exporter; type: ModuleType.Exporter;
format: ExportModuleOutputFormat; format: ExportModuleOutputFormat;

View File

@ -48,6 +48,8 @@ export interface ImportOptions {
// Only supported by some importers. // Only supported by some importers.
onProgress?: (progressState: any, progress?: any)=> void; onProgress?: (progressState: any, progress?: any)=> void;
onError?: (error: any)=> void; onError?: (error: any)=> void;
defaultFolderTitle?: string;
} }
export enum ExportProgressState { export enum ExportProgressState {

View File

@ -4,7 +4,7 @@
### Importing from Evernote ### Importing from Evernote
Joplin was designed as a replacement for Evernote and so can import complete Evernote notebooks, as well as notes, tags, resources (attached files) and note metadata (such as author, geo-location, etc.) via ENEX files. In terms of data, the only two things that might slightly differ are: Joplin can import complete Evernote notebooks, as well as notes, tags, resources (attached files) and note metadata (such as author, geo-location, etc.) via ENEX files. In terms of data, the only two things that might slightly differ are:
- Recognition data - Evernote images, in particular scanned (or photographed) documents have [recognition data](https://en.wikipedia.org/wiki/Optical_character_recognition) associated with them. It is the text that Evernote has been able to recognise in the document. This data is not preserved when the note are imported into Joplin. However, if you have enabled OCR in Joplin, that recognition data will be recreated in a format compatible with Joplin. - Recognition data - Evernote images, in particular scanned (or photographed) documents have [recognition data](https://en.wikipedia.org/wiki/Optical_character_recognition) associated with them. It is the text that Evernote has been able to recognise in the document. This data is not preserved when the note are imported into Joplin. However, if you have enabled OCR in Joplin, that recognition data will be recreated in a format compatible with Joplin.
@ -18,6 +18,12 @@ In the **desktop application**, open File > Import > ENEX and select your file.
In the **terminal application**, in [command-line mode](https://github.com/laurent22/joplin/blob/dev/readme/apps/terminal.md#command-line-mode), type `import /path/to/file.enex`. This will import the notes into a new notebook named after the filename. In the **terminal application**, in [command-line mode](https://github.com/laurent22/joplin/blob/dev/readme/apps/terminal.md#command-line-mode), type `import /path/to/file.enex`. This will import the notes into a new notebook named after the filename.
In both cases you can either import a single file or a directory that contains multiple ENEX files.
- If you import a single file, a notebook with the same name will be created, and all notes will be imported in this notebook.
- If you import a directory, Joplin will create a notebook per file and import the notes into them.
### Importing from Markdown files ### Importing from Markdown files
Joplin can import notes from plain Markdown file. You can either import a complete directory of Markdown files or individual files. Joplin can import notes from plain Markdown file. You can either import a complete directory of Markdown files or individual files.