2018-03-09 20:59:12 +00:00
const BaseModel = require ( 'lib/BaseModel.js' );
const BaseItem = require ( 'lib/models/BaseItem.js' );
2018-03-16 17:39:44 +00:00
const NoteResource = require ( 'lib/models/NoteResource.js' );
2018-11-13 00:45:08 +00:00
const ResourceLocalState = require ( 'lib/models/ResourceLocalState.js' );
2018-03-09 20:59:12 +00:00
const Setting = require ( 'lib/models/Setting.js' );
const ArrayUtils = require ( 'lib/ArrayUtils.js' );
const pathUtils = require ( 'lib/path-utils.js' );
const { mime } = require ( 'lib/mime-utils.js' );
2018-09-30 10:15:46 +01:00
const { filename , safeFilename } = require ( 'lib/path-utils.js' );
2018-03-09 20:59:12 +00:00
const { FsDriverDummy } = require ( 'lib/fs-driver-dummy.js' );
2018-05-23 12:14:38 +01:00
const markdownUtils = require ( 'lib/markdownUtils' );
2018-03-09 20:59:12 +00:00
const JoplinError = require ( 'lib/JoplinError' );
2017-06-24 19:51:43 +01:00
2017-07-02 13:02:07 +01:00
class Resource extends BaseItem {
2018-03-09 20:59:12 +00:00
2017-06-24 19:51:43 +01:00
static tableName () {
2018-03-09 20:59:12 +00:00
return 'resources' ;
2017-06-24 19:51:43 +01:00
}
2017-07-03 20:50:45 +01:00
static modelType () {
return BaseModel . TYPE_RESOURCE ;
2017-06-24 19:51:43 +01:00
}
2017-12-19 19:01:29 +00:00
static encryptionService () {
2018-03-09 20:59:12 +00:00
if ( ! this . encryptionService_ ) throw new Error ( 'Resource.encryptionService_ is not set!!' );
2017-12-19 19:01:29 +00:00
return this . encryptionService_ ;
}
2017-08-01 23:40:14 +02:00
static isSupportedImageMimeType ( type ) {
2018-10-11 12:18:33 -04:00
const imageMimeTypes = [ "image/jpg" , "image/jpeg" , "image/png" , "image/gif" , "image/svg+xml" , "image/webp" ];
2017-08-01 23:40:14 +02:00
return imageMimeTypes . indexOf ( type . toLowerCase ()) >= 0 ;
}
2018-10-08 19:11:53 +01:00
static needToBeFetched ( limit = null ) {
2018-11-13 00:45:08 +00:00
let sql = 'SELECT * FROM resources WHERE id IN (SELECT resource_id FROM resource_local_states WHERE fetch_status = ?) ORDER BY updated_time DESC' ;
2018-10-08 19:11:53 +01:00
if ( limit !== null ) sql += ' LIMIT ' + limit ;
return this . modelSelectAll ( sql , [ Resource . FETCH_STATUS_IDLE ]);
}
2018-11-13 22:25:23 +00:00
static async needToBeFetchedCount () {
const r = await this . db (). selectOne ( 'SELECT count(*) as total FROM resource_local_states WHERE fetch_status = ?' , [ Resource . FETCH_STATUS_IDLE ]);
return r ? r [ 'total' ] : 0 ;
}
2019-03-08 17:14:17 +00:00
static async resetStartedFetchStatus () {
return await this . db (). exec ( 'UPDATE resource_local_states SET fetch_status = ? WHERE fetch_status = ?' , [ Resource . FETCH_STATUS_IDLE , Resource . FETCH_STATUS_STARTED ]);
}
2017-07-05 22:52:31 +01:00
static fsDriver () {
if ( ! Resource . fsDriver_ ) Resource . fsDriver_ = new FsDriverDummy ();
return Resource . fsDriver_ ;
}
2017-12-19 19:01:29 +00:00
static filename ( resource , encryptedBlob = false ) {
2018-03-09 20:59:12 +00:00
let extension = encryptedBlob ? 'crypted' : resource . file_extension ;
if ( ! extension ) extension = resource . mime ? mime . toFileExtension ( resource . mime ) : '' ;
extension = extension ? ( '.' + extension ) : '' ;
2017-11-20 18:25:23 +00:00
return resource . id + extension ;
}
2018-09-30 10:15:46 +01:00
static friendlyFilename ( resource ) {
let output = safeFilename ( resource . title ); // Make sure not to allow spaces or any special characters as it's not supported in HTTP headers
if ( ! output ) output = resource . id ;
let extension = resource . file_extension ;
if ( ! extension ) extension = resource . mime ? mime . toFileExtension ( resource . mime ) : '' ;
extension = extension ? ( '.' + extension ) : '' ;
return output + extension ;
}
2019-01-20 16:27:33 +00:00
static baseDirectoryPath () {
return Setting . value ( 'resourceDir' );
}
2017-12-19 19:01:29 +00:00
static fullPath ( resource , encryptedBlob = false ) {
2018-03-09 20:59:12 +00:00
return Setting . value ( 'resourceDir' ) + '/' + this . filename ( resource , encryptedBlob );
2017-12-19 19:01:29 +00:00
}
2018-11-13 00:45:08 +00:00
static async isReady ( resource ) {
const ls = await this . localState ( resource );
return resource && ls . fetch_status === Resource . FETCH_STATUS_DONE && ! resource . encryption_blob_encrypted ;
2018-10-08 19:11:53 +01:00
}
2017-12-19 19:01:29 +00:00
// For resources, we need to decrypt the item (metadata) and the resource binary blob.
static async decrypt ( item ) {
2017-12-21 20:06:08 +01:00
// The item might already be decrypted but not the blob (for instance if it crashes while
// decrypting the blob or was otherwise interrupted).
const decryptedItem = item . encryption_cipher_text ? await super . decrypt ( item ) : Object . assign ({}, item );
2017-12-19 19:01:29 +00:00
if ( ! decryptedItem . encryption_blob_encrypted ) return decryptedItem ;
const plainTextPath = this . fullPath ( decryptedItem );
const encryptedPath = this . fullPath ( decryptedItem , true );
2018-03-09 20:59:12 +00:00
const noExtPath = pathUtils . dirname ( encryptedPath ) + '/' + pathUtils . filename ( encryptedPath );
2018-10-11 12:18:33 -04:00
2017-12-19 19:01:29 +00:00
// When the resource blob is downloaded by the synchroniser, it's initially a file with no
// extension (since it's encrypted, so we don't know its extension). So here rename it
// to a file with a ".crypted" extension so that it's better identified, and then decrypt it.
// Potentially plainTextPath is also a path with no extension if it's an unknown mime type.
if ( await this . fsDriver (). exists ( noExtPath )) {
await this . fsDriver (). move ( noExtPath , encryptedPath );
}
2018-04-22 14:33:12 +02:00
try {
2018-06-25 18:14:57 +01:00
// const stat = await this.fsDriver().stat(encryptedPath);
await this . encryptionService (). decryptFile ( encryptedPath , plainTextPath , {
// onProgress: (progress) => {
// console.info('Decryption: ', progress.doneSize / stat.size);
// },
});
2018-04-22 14:33:12 +02:00
} catch ( error ) {
if ( error . code === 'invalidIdentifier' ) {
// As the identifier is invalid it most likely means that this is not encrypted data
// at all. It can happen for example when there's a crash between the moment the data
// is decrypted and the resource item is updated.
this . logger (). warn ( 'Found a resource that was most likely already decrypted but was marked as encrypted. Marked it as decrypted: ' + item . id )
this . fsDriver (). move ( encryptedPath , plainTextPath );
} else {
throw error ;
}
}
2018-01-02 20:17:14 +01:00
2017-12-21 20:06:08 +01:00
decryptedItem . encryption_blob_encrypted = 0 ;
2018-01-02 20:17:14 +01:00
return super . save ( decryptedItem , { autoTimestamp : false });
2017-12-19 19:01:29 +00:00
}
// Prepare the resource by encrypting it if needed.
2017-12-04 19:01:56 +00:00
// The call returns the path to the physical file AND a representation of the resource object
// as it should be uploaded to the sync target. Note that this may be different from what is stored
// in the database. In particular, the flag encryption_blob_encrypted might be 1 on the sync target
// if the resource is encrypted, but will be 0 locally because the device has the decrypted resource.
2017-12-19 19:01:29 +00:00
static async fullPathForSyncUpload ( resource ) {
const plainTextPath = this . fullPath ( resource );
2018-03-09 20:59:12 +00:00
if ( ! Setting . value ( 'encryption.enabled' )) {
2018-01-02 20:17:14 +01:00
// Normally not possible since itemsThatNeedSync should only return decrypted items
2018-03-09 20:59:12 +00:00
if ( !! resource . encryption_blob_encrypted ) throw new Error ( 'Trying to access encrypted resource but encryption is currently disabled' );
2017-12-19 19:01:29 +00:00
return { path : plainTextPath , resource : resource };
}
const encryptedPath = this . fullPath ( resource , true );
if ( resource . encryption_blob_encrypted ) return { path : encryptedPath , resource : resource };
2018-01-28 17:37:03 +00:00
try {
2018-06-25 18:14:57 +01:00
// const stat = await this.fsDriver().stat(plainTextPath);
await this . encryptionService (). encryptFile ( plainTextPath , encryptedPath , {
// onProgress: (progress) => {
// console.info(progress.doneSize / stat.size);
// },
});
2018-01-28 17:37:03 +00:00
} catch ( error ) {
2018-03-09 20:59:12 +00:00
if ( error . code === 'ENOENT' ) throw new JoplinError ( 'File not found:' + error . toString (), 'fileNotFound' );
2018-01-28 17:37:03 +00:00
throw error ;
}
2017-12-19 19:01:29 +00:00
2017-12-04 19:01:56 +00:00
const resourceCopy = Object . assign ({}, resource );
resourceCopy . encryption_blob_encrypted = 1 ;
return { path : encryptedPath , resource : resourceCopy };
2017-06-24 19:51:43 +01:00
}
2017-08-01 23:40:14 +02:00
static markdownTag ( resource ) {
let tagAlt = resource . alt ? resource . alt : resource . title ;
2018-03-09 20:59:12 +00:00
if ( ! tagAlt ) tagAlt = '' ;
2017-08-01 23:40:14 +02:00
let lines = [];
if ( Resource . isSupportedImageMimeType ( resource . mime )) {
lines . push ( "" );
} else {
lines . push ( "[" );
2017-08-02 17:47:25 +00:00
lines . push ( markdownUtils . escapeLinkText ( tagAlt ));
2017-08-01 23:40:14 +02:00
lines . push ( "](:/" + resource . id + ")" );
}
2018-03-09 20:59:12 +00:00
return lines . join ( '' );
2017-08-01 23:40:14 +02:00
}
2018-05-23 12:14:38 +01:00
static internalUrl ( resource ) {
return ':/' + resource . id ;
}
2017-06-25 00:19:11 +01:00
static pathToId ( path ) {
return filename ( path );
}
2017-10-24 20:40:15 +01:00
static async content ( resource ) {
2018-03-09 20:59:12 +00:00
return this . fsDriver (). readFile ( this . fullPath ( resource ), 'Buffer' );
2017-07-02 13:02:07 +01:00
}
static setContent ( resource , content ) {
2017-07-05 22:52:31 +01:00
return this . fsDriver (). writeBinaryFile ( this . fullPath ( resource ), content );
2017-07-02 13:02:07 +01:00
}
2017-07-26 19:36:16 +01:00
static isResourceUrl ( url ) {
2018-03-09 20:59:12 +00:00
return url && url . length === 34 && url [ 0 ] === ':' && url [ 1 ] === '/' ;
2017-07-26 19:36:16 +01:00
}
static urlToId ( url ) {
2018-03-09 20:59:12 +00:00
if ( ! this . isResourceUrl ( url )) throw new Error ( 'Not a valid resource URL: ' + url );
2017-07-26 19:36:16 +01:00
return url . substr ( 2 );
}
2018-03-09 20:59:12 +00:00
2018-11-13 00:45:08 +00:00
static localState ( resourceOrId ) {
return ResourceLocalState . byResourceId ( typeof resourceOrId === 'object' ? resourceOrId . id : resourceOrId );
}
static async setLocalState ( resourceOrId , state ) {
const id = typeof resourceOrId === 'object' ? resourceOrId . id : resourceOrId ;
await ResourceLocalState . save ( Object . assign ({}, state , { resource_id : id }));
}
2018-03-15 17:46:54 +00:00
static async batchDelete ( ids , options = null ) {
// For resources, there's not really batch deleting since there's the file data to delete
// too, so each is processed one by one with the item being deleted last (since the db
// call is the less likely to fail).
for ( let i = 0 ; i < ids . length ; i ++ ) {
const id = ids [ i ];
const resource = await Resource . load ( id );
2018-04-22 14:33:12 +02:00
if ( ! resource ) continue ;
2018-03-15 17:46:54 +00:00
const path = Resource . fullPath ( resource );
await this . fsDriver (). remove ( path );
2018-03-16 17:39:44 +00:00
await super . batchDelete ([ id ], options );
await NoteResource . deleteByResource ( id ); // Clean up note/resource relationships
2018-03-15 17:46:54 +00:00
}
2018-11-13 00:45:08 +00:00
await ResourceLocalState . batchDelete ( ids );
2018-03-15 17:46:54 +00:00
}
2017-06-24 19:51:43 +01:00
}
2017-10-19 23:02:13 +01:00
Resource . IMAGE_MAX_DIMENSION = 1920 ;
2018-10-07 20:11:33 +01:00
Resource . FETCH_STATUS_IDLE = 0 ;
Resource . FETCH_STATUS_STARTED = 1 ;
Resource . FETCH_STATUS_DONE = 2 ;
Resource . FETCH_STATUS_ERROR = 3 ;
2018-03-09 20:59:12 +00:00
module . exports = Resource ;