2018-03-09 22:59:12 +02:00
const BaseModel = require ( 'lib/BaseModel.js' ) ;
const BaseItem = require ( 'lib/models/BaseItem.js' ) ;
2018-03-16 19:39:44 +02:00
const NoteResource = require ( 'lib/models/NoteResource.js' ) ;
2018-11-13 02:45:08 +02:00
const ResourceLocalState = require ( 'lib/models/ResourceLocalState.js' ) ;
2018-03-09 22:59:12 +02: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 11:15:46 +02:00
const { filename , safeFilename } = require ( 'lib/path-utils.js' ) ;
2018-03-09 22:59:12 +02:00
const { FsDriverDummy } = require ( 'lib/fs-driver-dummy.js' ) ;
2018-05-23 13:14:38 +02:00
const markdownUtils = require ( 'lib/markdownUtils' ) ;
2018-03-09 22:59:12 +02:00
const JoplinError = require ( 'lib/JoplinError' ) ;
2017-06-24 20:51:43 +02:00
2017-07-02 14:02:07 +02:00
class Resource extends BaseItem {
2018-03-09 22:59:12 +02:00
2017-06-24 20:51:43 +02:00
static tableName ( ) {
2018-03-09 22:59:12 +02:00
return 'resources' ;
2017-06-24 20:51:43 +02:00
}
2017-07-03 21:50:45 +02:00
static modelType ( ) {
return BaseModel . TYPE _RESOURCE ;
2017-06-24 20:51:43 +02:00
}
2017-12-19 21:01:29 +02:00
static encryptionService ( ) {
2018-03-09 22:59:12 +02:00
if ( ! this . encryptionService _ ) throw new Error ( 'Resource.encryptionService_ is not set!!' ) ;
2017-12-19 21:01:29 +02:00
return this . encryptionService _ ;
}
2017-08-01 23:40:14 +02:00
static isSupportedImageMimeType ( type ) {
2018-10-11 18:18:33 +02: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 20:11:53 +02:00
static needToBeFetched ( limit = null ) {
2018-11-13 02:45:08 +02: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 20:11:53 +02:00
if ( limit !== null ) sql += ' LIMIT ' + limit ;
return this . modelSelectAll ( sql , [ Resource . FETCH _STATUS _IDLE ] ) ;
}
2018-11-14 00:25:23 +02: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 19:14:17 +02: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 23:52:31 +02:00
static fsDriver ( ) {
if ( ! Resource . fsDriver _ ) Resource . fsDriver _ = new FsDriverDummy ( ) ;
return Resource . fsDriver _ ;
}
2017-12-19 21:01:29 +02:00
static filename ( resource , encryptedBlob = false ) {
2018-03-09 22:59:12 +02:00
let extension = encryptedBlob ? 'crypted' : resource . file _extension ;
if ( ! extension ) extension = resource . mime ? mime . toFileExtension ( resource . mime ) : '' ;
extension = extension ? ( '.' + extension ) : '' ;
2017-11-20 20:25:23 +02:00
return resource . id + extension ;
}
2018-09-30 11:15:46 +02: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 18:27:33 +02:00
static baseDirectoryPath ( ) {
return Setting . value ( 'resourceDir' ) ;
}
2017-12-19 21:01:29 +02:00
static fullPath ( resource , encryptedBlob = false ) {
2018-03-09 22:59:12 +02:00
return Setting . value ( 'resourceDir' ) + '/' + this . filename ( resource , encryptedBlob ) ;
2017-12-19 21:01:29 +02:00
}
2018-11-13 02:45:08 +02: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 20:11:53 +02:00
}
2017-12-19 21:01:29 +02:00
// For resources, we need to decrypt the item (metadata) and the resource binary blob.
static async decrypt ( item ) {
2017-12-21 21:06:08 +02: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 21:01:29 +02:00
if ( ! decryptedItem . encryption _blob _encrypted ) return decryptedItem ;
const plainTextPath = this . fullPath ( decryptedItem ) ;
const encryptedPath = this . fullPath ( decryptedItem , true ) ;
2018-03-09 22:59:12 +02:00
const noExtPath = pathUtils . dirname ( encryptedPath ) + '/' + pathUtils . filename ( encryptedPath ) ;
2018-10-11 18:18:33 +02:00
2017-12-19 21:01:29 +02: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 19:14:57 +02: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 21:17:14 +02:00
2017-12-21 21:06:08 +02:00
decryptedItem . encryption _blob _encrypted = 0 ;
2018-01-02 21:17:14 +02:00
return super . save ( decryptedItem , { autoTimestamp : false } ) ;
2017-12-19 21:01:29 +02:00
}
// Prepare the resource by encrypting it if needed.
2017-12-04 21:01:56 +02: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 21:01:29 +02:00
static async fullPathForSyncUpload ( resource ) {
const plainTextPath = this . fullPath ( resource ) ;
2018-03-09 22:59:12 +02:00
if ( ! Setting . value ( 'encryption.enabled' ) ) {
2018-01-02 21:17:14 +02:00
// Normally not possible since itemsThatNeedSync should only return decrypted items
2018-03-09 22:59:12 +02:00
if ( ! ! resource . encryption _blob _encrypted ) throw new Error ( 'Trying to access encrypted resource but encryption is currently disabled' ) ;
2017-12-19 21:01:29 +02: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 19:37:03 +02:00
try {
2018-06-25 19:14:57 +02: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 19:37:03 +02:00
} catch ( error ) {
2018-03-09 22:59:12 +02:00
if ( error . code === 'ENOENT' ) throw new JoplinError ( 'File not found:' + error . toString ( ) , 'fileNotFound' ) ;
2018-01-28 19:37:03 +02:00
throw error ;
}
2017-12-19 21:01:29 +02:00
2017-12-04 21:01:56 +02:00
const resourceCopy = Object . assign ( { } , resource ) ;
resourceCopy . encryption _blob _encrypted = 1 ;
return { path : encryptedPath , resource : resourceCopy } ;
2017-06-24 20:51:43 +02:00
}
2017-08-01 23:40:14 +02:00
static markdownTag ( resource ) {
let tagAlt = resource . alt ? resource . alt : resource . title ;
2018-03-09 22:59:12 +02:00
if ( ! tagAlt ) tagAlt = '' ;
2017-08-01 23:40:14 +02:00
let lines = [ ] ;
if ( Resource . isSupportedImageMimeType ( resource . mime ) ) {
lines . push ( "![" ) ;
2017-08-02 19:47:25 +02:00
lines . push ( markdownUtils . escapeLinkText ( tagAlt ) ) ;
2017-08-01 23:40:14 +02:00
lines . push ( "](:/" + resource . id + ")" ) ;
} else {
lines . push ( "[" ) ;
2017-08-02 19:47:25 +02:00
lines . push ( markdownUtils . escapeLinkText ( tagAlt ) ) ;
2017-08-01 23:40:14 +02:00
lines . push ( "](:/" + resource . id + ")" ) ;
}
2018-03-09 22:59:12 +02:00
return lines . join ( '' ) ;
2017-08-01 23:40:14 +02:00
}
2018-05-23 13:14:38 +02:00
static internalUrl ( resource ) {
return ':/' + resource . id ;
}
2017-06-25 01:19:11 +02:00
static pathToId ( path ) {
return filename ( path ) ;
}
2017-10-24 21:40:15 +02:00
static async content ( resource ) {
2018-03-09 22:59:12 +02:00
return this . fsDriver ( ) . readFile ( this . fullPath ( resource ) , 'Buffer' ) ;
2017-07-02 14:02:07 +02:00
}
static setContent ( resource , content ) {
2017-07-05 23:52:31 +02:00
return this . fsDriver ( ) . writeBinaryFile ( this . fullPath ( resource ) , content ) ;
2017-07-02 14:02:07 +02:00
}
2017-07-26 20:36:16 +02:00
static isResourceUrl ( url ) {
2018-03-09 22:59:12 +02:00
return url && url . length === 34 && url [ 0 ] === ':' && url [ 1 ] === '/' ;
2017-07-26 20:36:16 +02:00
}
static urlToId ( url ) {
2018-03-09 22:59:12 +02:00
if ( ! this . isResourceUrl ( url ) ) throw new Error ( 'Not a valid resource URL: ' + url ) ;
2017-07-26 20:36:16 +02:00
return url . substr ( 2 ) ;
}
2018-03-09 22:59:12 +02:00
2018-11-13 02:45:08 +02: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 19:46:54 +02: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 19:46:54 +02:00
const path = Resource . fullPath ( resource ) ;
await this . fsDriver ( ) . remove ( path ) ;
2018-03-16 19:39:44 +02:00
await super . batchDelete ( [ id ] , options ) ;
await NoteResource . deleteByResource ( id ) ; // Clean up note/resource relationships
2018-03-15 19:46:54 +02:00
}
2018-11-13 02:45:08 +02:00
await ResourceLocalState . batchDelete ( ids ) ;
2018-03-15 19:46:54 +02:00
}
2017-06-24 20:51:43 +02:00
}
2017-10-20 00:02:13 +02:00
Resource . IMAGE _MAX _DIMENSION = 1920 ;
2018-10-07 21:11:33 +02:00
Resource . FETCH _STATUS _IDLE = 0 ;
Resource . FETCH _STATUS _STARTED = 1 ;
Resource . FETCH _STATUS _DONE = 2 ;
Resource . FETCH _STATUS _ERROR = 3 ;
2018-03-09 22:59:12 +02:00
module . exports = Resource ;