2016-09-29 03:54:25 +00:00
|
|
|
/* jslint node: true */
|
|
|
|
'use strict';
|
|
|
|
|
2022-06-05 20:04:25 +00:00
|
|
|
const fileDb = require('./database.js').dbs.file;
|
|
|
|
const Errors = require('./enig_error.js').Errors;
|
|
|
|
const { getISOTimestampString, sanitizeString } = require('./database.js');
|
|
|
|
const Config = require('./config.js').get;
|
2018-06-23 03:26:46 +00:00
|
|
|
|
|
|
|
// deps
|
2022-06-05 20:04:25 +00:00
|
|
|
const async = require('async');
|
|
|
|
const _ = require('lodash');
|
|
|
|
const paths = require('path');
|
|
|
|
const fse = require('fs-extra');
|
|
|
|
const { unlink, readFile } = require('graceful-fs');
|
|
|
|
const crypto = require('crypto');
|
|
|
|
const moment = require('moment');
|
|
|
|
|
|
|
|
const FILE_TABLE_MEMBERS = [
|
|
|
|
'file_id',
|
|
|
|
'area_tag',
|
|
|
|
'file_sha256',
|
|
|
|
'file_name',
|
|
|
|
'storage_tag',
|
|
|
|
'desc',
|
|
|
|
'desc_long',
|
|
|
|
'upload_timestamp',
|
2016-09-29 03:54:25 +00:00
|
|
|
];
|
|
|
|
|
2016-09-29 04:26:06 +00:00
|
|
|
const FILE_WELL_KNOWN_META = {
|
2018-06-23 03:26:46 +00:00
|
|
|
// name -> *read* converter, if any
|
2022-06-05 20:04:25 +00:00
|
|
|
upload_by_username: null,
|
|
|
|
upload_by_user_id: u => parseInt(u) || 0,
|
|
|
|
file_md5: null,
|
|
|
|
file_sha1: null,
|
|
|
|
file_crc32: null,
|
|
|
|
est_release_year: y => parseInt(y) || new Date().getFullYear(),
|
|
|
|
dl_count: d => parseInt(d) || 0,
|
|
|
|
byte_size: b => parseInt(b) || 0,
|
|
|
|
archive_type: null,
|
|
|
|
short_file_name: null, // e.g. DOS 8.3 filename, avail in some scenarios such as TIC import
|
|
|
|
tic_origin: null, // TIC "Origin"
|
|
|
|
tic_desc: null, // TIC "Desc"
|
|
|
|
tic_ldesc: null, // TIC "Ldesc" joined by '\n'
|
|
|
|
session_temp_dl: v => (parseInt(v) ? true : false),
|
|
|
|
desc_sauce: s => JSON.parse(s) || {},
|
|
|
|
desc_long_sauce: s => JSON.parse(s) || {},
|
2016-09-29 04:26:06 +00:00
|
|
|
};
|
|
|
|
|
2016-09-29 03:54:25 +00:00
|
|
|
module.exports = class FileEntry {
|
2018-06-22 05:15:04 +00:00
|
|
|
constructor(options) {
|
2022-06-05 20:04:25 +00:00
|
|
|
options = options || {};
|
2018-06-23 03:26:46 +00:00
|
|
|
|
2022-06-05 20:04:25 +00:00
|
|
|
this.fileId = options.fileId || 0;
|
|
|
|
this.areaTag = options.areaTag || '';
|
|
|
|
this.meta = Object.assign({ dl_count: 0 }, options.meta);
|
|
|
|
this.hashTags = options.hashTags || new Set();
|
|
|
|
this.fileName = options.fileName;
|
2018-06-23 03:26:46 +00:00
|
|
|
this.storageTag = options.storageTag;
|
|
|
|
this.fileSha256 = options.fileSha256;
|
2018-06-22 05:15:04 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
static loadBasicEntry(fileId, dest, cb) {
|
|
|
|
dest = dest || {};
|
|
|
|
|
|
|
|
fileDb.get(
|
|
|
|
`SELECT ${FILE_TABLE_MEMBERS.join(', ')}
|
2018-06-23 03:26:46 +00:00
|
|
|
FROM file
|
|
|
|
WHERE file_id=?
|
|
|
|
LIMIT 1;`,
|
2022-06-05 20:04:25 +00:00
|
|
|
[fileId],
|
2018-06-22 05:15:04 +00:00
|
|
|
(err, file) => {
|
2022-06-05 20:04:25 +00:00
|
|
|
if (err) {
|
2018-06-22 05:15:04 +00:00
|
|
|
return cb(err);
|
|
|
|
}
|
|
|
|
|
2022-06-05 20:04:25 +00:00
|
|
|
if (!file) {
|
2018-06-22 05:15:04 +00:00
|
|
|
return cb(Errors.DoesNotExist('No file is available by that ID'));
|
|
|
|
}
|
|
|
|
|
2018-06-23 03:26:46 +00:00
|
|
|
// assign props from |file|
|
2018-06-22 05:15:04 +00:00
|
|
|
FILE_TABLE_MEMBERS.forEach(prop => {
|
|
|
|
dest[_.camelCase(prop)] = file[prop];
|
|
|
|
});
|
|
|
|
|
|
|
|
return cb(null, dest);
|
|
|
|
}
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
load(fileId, cb) {
|
|
|
|
const self = this;
|
|
|
|
|
|
|
|
async.series(
|
|
|
|
[
|
|
|
|
function loadBasicEntry(callback) {
|
|
|
|
FileEntry.loadBasicEntry(fileId, self, callback);
|
|
|
|
},
|
|
|
|
function loadMeta(callback) {
|
|
|
|
return self.loadMeta(callback);
|
|
|
|
},
|
|
|
|
function loadHashTags(callback) {
|
|
|
|
return self.loadHashTags(callback);
|
|
|
|
},
|
|
|
|
function loadUserRating(callback) {
|
|
|
|
return self.loadRating(callback);
|
2022-06-05 20:04:25 +00:00
|
|
|
},
|
2018-06-22 05:15:04 +00:00
|
|
|
],
|
|
|
|
err => {
|
|
|
|
return cb(err);
|
|
|
|
}
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
persist(isUpdate, cb) {
|
2022-06-05 20:04:25 +00:00
|
|
|
if (!cb && _.isFunction(isUpdate)) {
|
2018-06-22 05:15:04 +00:00
|
|
|
cb = isUpdate;
|
|
|
|
isUpdate = false;
|
|
|
|
}
|
|
|
|
|
|
|
|
const self = this;
|
|
|
|
|
|
|
|
async.waterfall(
|
|
|
|
[
|
|
|
|
function check(callback) {
|
2022-06-05 20:04:25 +00:00
|
|
|
if (isUpdate && !self.fileId) {
|
|
|
|
return callback(
|
|
|
|
Errors.Invalid(
|
|
|
|
'Cannot update file entry without an existing "fileId" member'
|
|
|
|
)
|
|
|
|
);
|
2018-06-22 05:15:04 +00:00
|
|
|
}
|
|
|
|
return callback(null);
|
|
|
|
},
|
|
|
|
function calcSha256IfNeeded(callback) {
|
2022-06-05 20:04:25 +00:00
|
|
|
if (self.fileSha256) {
|
2018-06-22 05:15:04 +00:00
|
|
|
return callback(null);
|
|
|
|
}
|
|
|
|
|
2022-06-05 20:04:25 +00:00
|
|
|
if (isUpdate) {
|
|
|
|
return callback(
|
|
|
|
Errors.MissingParam(
|
|
|
|
'fileSha256 property must be set for updates!'
|
|
|
|
)
|
|
|
|
);
|
2018-06-22 05:15:04 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
readFile(self.filePath, (err, data) => {
|
2022-06-05 20:04:25 +00:00
|
|
|
if (err) {
|
2018-06-22 05:15:04 +00:00
|
|
|
return callback(err);
|
|
|
|
}
|
|
|
|
|
|
|
|
const sha256 = crypto.createHash('sha256');
|
|
|
|
sha256.update(data);
|
|
|
|
self.fileSha256 = sha256.digest('hex');
|
|
|
|
return callback(null);
|
|
|
|
});
|
|
|
|
},
|
|
|
|
function startTrans(callback) {
|
|
|
|
return fileDb.beginTransaction(callback);
|
|
|
|
},
|
|
|
|
function storeEntry(trans, callback) {
|
2022-06-05 20:04:25 +00:00
|
|
|
if (isUpdate) {
|
2018-06-22 05:15:04 +00:00
|
|
|
trans.run(
|
|
|
|
`REPLACE INTO file (file_id, area_tag, file_sha256, file_name, storage_tag, desc, desc_long, upload_timestamp)
|
2018-06-23 03:26:46 +00:00
|
|
|
VALUES(?, ?, ?, ?, ?, ?, ?, ?);`,
|
2022-06-05 20:04:25 +00:00
|
|
|
[
|
|
|
|
self.fileId,
|
|
|
|
self.areaTag,
|
|
|
|
self.fileSha256,
|
|
|
|
self.fileName,
|
|
|
|
self.storageTag,
|
|
|
|
self.desc,
|
|
|
|
self.descLong,
|
|
|
|
getISOTimestampString(),
|
|
|
|
],
|
2018-06-22 05:15:04 +00:00
|
|
|
err => {
|
|
|
|
return callback(err, trans);
|
|
|
|
}
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
trans.run(
|
|
|
|
`REPLACE INTO file (area_tag, file_sha256, file_name, storage_tag, desc, desc_long, upload_timestamp)
|
2018-06-23 03:26:46 +00:00
|
|
|
VALUES(?, ?, ?, ?, ?, ?, ?);`,
|
2022-06-05 20:04:25 +00:00
|
|
|
[
|
|
|
|
self.areaTag,
|
|
|
|
self.fileSha256,
|
|
|
|
self.fileName,
|
|
|
|
self.storageTag,
|
|
|
|
self.desc,
|
|
|
|
self.descLong,
|
|
|
|
getISOTimestampString(),
|
|
|
|
],
|
|
|
|
function inserted(err) {
|
|
|
|
// use non-arrow func for 'this' scope / lastID
|
|
|
|
if (!err) {
|
2018-06-22 05:15:04 +00:00
|
|
|
self.fileId = this.lastID;
|
|
|
|
}
|
|
|
|
return callback(err, trans);
|
|
|
|
}
|
|
|
|
);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
function storeMeta(trans, callback) {
|
2022-06-05 20:04:25 +00:00
|
|
|
async.each(
|
|
|
|
Object.keys(self.meta),
|
|
|
|
(n, next) => {
|
|
|
|
const v = self.meta[n];
|
|
|
|
return FileEntry.persistMetaValue(
|
|
|
|
self.fileId,
|
|
|
|
n,
|
|
|
|
v,
|
|
|
|
trans,
|
|
|
|
next
|
|
|
|
);
|
|
|
|
},
|
|
|
|
err => {
|
|
|
|
return callback(err, trans);
|
|
|
|
}
|
|
|
|
);
|
2018-06-22 05:15:04 +00:00
|
|
|
},
|
|
|
|
function storeHashTags(trans, callback) {
|
|
|
|
const hashTagsArray = Array.from(self.hashTags);
|
2022-06-05 20:04:25 +00:00
|
|
|
async.each(
|
|
|
|
hashTagsArray,
|
|
|
|
(hashTag, next) => {
|
|
|
|
return FileEntry.persistHashTag(
|
|
|
|
self.fileId,
|
|
|
|
hashTag,
|
|
|
|
trans,
|
|
|
|
next
|
|
|
|
);
|
|
|
|
},
|
|
|
|
err => {
|
|
|
|
return callback(err, trans);
|
|
|
|
}
|
|
|
|
);
|
|
|
|
},
|
2018-06-22 05:15:04 +00:00
|
|
|
],
|
|
|
|
(err, trans) => {
|
2018-06-23 03:26:46 +00:00
|
|
|
// :TODO: Log orig err
|
2022-06-05 20:04:25 +00:00
|
|
|
if (trans) {
|
2018-06-22 05:15:04 +00:00
|
|
|
trans[err ? 'rollback' : 'commit'](transErr => {
|
|
|
|
return cb(transErr ? transErr : err);
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
return cb(err);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
static getAreaStorageDirectoryByTag(storageTag) {
|
|
|
|
const config = Config();
|
2022-06-05 20:04:25 +00:00
|
|
|
const storageLocation = storageTag && config.fileBase.storageTags[storageTag];
|
2018-06-22 05:15:04 +00:00
|
|
|
|
2018-06-23 03:26:46 +00:00
|
|
|
// absolute paths as-is
|
2022-06-05 20:04:25 +00:00
|
|
|
if (storageLocation && '/' === storageLocation.charAt(0)) {
|
2018-06-22 05:15:04 +00:00
|
|
|
return storageLocation;
|
|
|
|
}
|
|
|
|
|
2018-06-23 03:26:46 +00:00
|
|
|
// relative to |areaStoragePrefix|
|
2018-06-22 05:15:04 +00:00
|
|
|
return paths.join(config.fileBase.areaStoragePrefix, storageLocation || '');
|
|
|
|
}
|
|
|
|
|
|
|
|
get filePath() {
|
|
|
|
const storageDir = FileEntry.getAreaStorageDirectoryByTag(this.storageTag);
|
|
|
|
return paths.join(storageDir, this.fileName);
|
|
|
|
}
|
|
|
|
|
|
|
|
static quickCheckExistsByPath(fullPath, cb) {
|
|
|
|
fileDb.get(
|
|
|
|
`SELECT COUNT() AS count
|
2018-06-23 03:26:46 +00:00
|
|
|
FROM file
|
|
|
|
WHERE file_name = ?
|
|
|
|
LIMIT 1;`,
|
2022-06-05 20:04:25 +00:00
|
|
|
[paths.basename(fullPath)],
|
2018-06-22 05:15:04 +00:00
|
|
|
(err, rows) => {
|
|
|
|
return err ? cb(err) : cb(null, rows.count > 0 ? true : false);
|
|
|
|
}
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
static persistUserRating(fileId, userId, rating, cb) {
|
|
|
|
return fileDb.run(
|
|
|
|
`REPLACE INTO file_user_rating (file_id, user_id, rating)
|
2018-06-23 03:26:46 +00:00
|
|
|
VALUES (?, ?, ?);`,
|
2022-06-05 20:04:25 +00:00
|
|
|
[fileId, userId, rating],
|
2018-06-22 05:15:04 +00:00
|
|
|
cb
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2020-11-29 23:24:51 +00:00
|
|
|
static removeUserRatings(userId, cb) {
|
|
|
|
return fileDb.run(
|
|
|
|
`DELETE FROM file_user_rating
|
|
|
|
WHERE user_id = ?;`,
|
2022-06-05 20:04:25 +00:00
|
|
|
[userId],
|
2020-11-29 23:24:51 +00:00
|
|
|
cb
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2018-06-22 05:15:04 +00:00
|
|
|
static persistMetaValue(fileId, name, value, transOrDb, cb) {
|
2022-06-05 20:04:25 +00:00
|
|
|
if (!_.isFunction(cb) && _.isFunction(transOrDb)) {
|
2018-06-22 05:15:04 +00:00
|
|
|
cb = transOrDb;
|
|
|
|
transOrDb = fileDb;
|
|
|
|
}
|
|
|
|
|
|
|
|
return transOrDb.run(
|
|
|
|
`REPLACE INTO file_meta (file_id, meta_name, meta_value)
|
2018-06-23 03:26:46 +00:00
|
|
|
VALUES (?, ?, ?);`,
|
2022-06-05 20:04:25 +00:00
|
|
|
[fileId, name, value],
|
2018-06-22 05:15:04 +00:00
|
|
|
cb
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
static incrementAndPersistMetaValue(fileId, name, incrementBy, cb) {
|
|
|
|
incrementBy = incrementBy || 1;
|
|
|
|
fileDb.run(
|
|
|
|
`UPDATE file_meta
|
2018-06-23 03:26:46 +00:00
|
|
|
SET meta_value = meta_value + ?
|
|
|
|
WHERE file_id = ? AND meta_name = ?;`,
|
2022-06-05 20:04:25 +00:00
|
|
|
[incrementBy, fileId, name],
|
2018-06-22 05:15:04 +00:00
|
|
|
err => {
|
2022-06-05 20:04:25 +00:00
|
|
|
if (cb) {
|
2018-06-22 05:15:04 +00:00
|
|
|
return cb(err);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
loadMeta(cb) {
|
|
|
|
fileDb.each(
|
|
|
|
`SELECT meta_name, meta_value
|
2018-06-23 03:26:46 +00:00
|
|
|
FROM file_meta
|
|
|
|
WHERE file_id=?;`,
|
2022-06-05 20:04:25 +00:00
|
|
|
[this.fileId],
|
2018-06-22 05:15:04 +00:00
|
|
|
(err, meta) => {
|
2022-06-05 20:04:25 +00:00
|
|
|
if (meta) {
|
2018-06-22 05:15:04 +00:00
|
|
|
const conv = FILE_WELL_KNOWN_META[meta.meta_name];
|
2022-06-05 20:04:25 +00:00
|
|
|
this.meta[meta.meta_name] = conv
|
|
|
|
? conv(meta.meta_value)
|
|
|
|
: meta.meta_value;
|
2018-06-22 05:15:04 +00:00
|
|
|
}
|
|
|
|
},
|
|
|
|
err => {
|
|
|
|
return cb(err);
|
|
|
|
}
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
static persistHashTag(fileId, hashTag, transOrDb, cb) {
|
2022-06-05 20:04:25 +00:00
|
|
|
if (!_.isFunction(cb) && _.isFunction(transOrDb)) {
|
2018-06-22 05:15:04 +00:00
|
|
|
cb = transOrDb;
|
|
|
|
transOrDb = fileDb;
|
|
|
|
}
|
|
|
|
|
2022-06-05 20:04:25 +00:00
|
|
|
transOrDb.serialize(() => {
|
2018-06-22 05:15:04 +00:00
|
|
|
transOrDb.run(
|
|
|
|
`INSERT OR IGNORE INTO hash_tag (hash_tag)
|
2018-06-23 03:26:46 +00:00
|
|
|
VALUES (?);`,
|
2022-06-05 20:04:25 +00:00
|
|
|
[hashTag]
|
2018-06-22 05:15:04 +00:00
|
|
|
);
|
2017-01-19 05:23:53 +00:00
|
|
|
|
2018-06-22 05:15:04 +00:00
|
|
|
transOrDb.run(
|
|
|
|
`REPLACE INTO file_hash_tag (hash_tag_id, file_id)
|
2018-06-23 03:26:46 +00:00
|
|
|
VALUES (
|
|
|
|
(SELECT hash_tag_id
|
|
|
|
FROM hash_tag
|
|
|
|
WHERE hash_tag = ?),
|
|
|
|
?
|
|
|
|
);`,
|
2022-06-05 20:04:25 +00:00
|
|
|
[hashTag, fileId],
|
2018-06-22 05:15:04 +00:00
|
|
|
err => {
|
|
|
|
return cb(err);
|
|
|
|
}
|
|
|
|
);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
loadHashTags(cb) {
|
|
|
|
fileDb.each(
|
|
|
|
`SELECT ht.hash_tag_id, ht.hash_tag
|
2018-06-23 03:26:46 +00:00
|
|
|
FROM hash_tag ht
|
|
|
|
WHERE ht.hash_tag_id IN (
|
|
|
|
SELECT hash_tag_id
|
|
|
|
FROM file_hash_tag
|
|
|
|
WHERE file_id=?
|
|
|
|
);`,
|
2022-06-05 20:04:25 +00:00
|
|
|
[this.fileId],
|
2018-06-22 05:15:04 +00:00
|
|
|
(err, hashTag) => {
|
2022-06-05 20:04:25 +00:00
|
|
|
if (hashTag) {
|
2018-06-22 05:15:04 +00:00
|
|
|
this.hashTags.add(hashTag.hash_tag);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
err => {
|
|
|
|
return cb(err);
|
|
|
|
}
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
loadRating(cb) {
|
|
|
|
fileDb.get(
|
|
|
|
`SELECT AVG(fur.rating) AS avg_rating
|
2018-06-23 03:26:46 +00:00
|
|
|
FROM file_user_rating fur
|
|
|
|
INNER JOIN file f
|
|
|
|
ON f.file_id = fur.file_id
|
|
|
|
AND f.file_id = ?`,
|
2022-06-05 20:04:25 +00:00
|
|
|
[this.fileId],
|
2018-06-22 05:15:04 +00:00
|
|
|
(err, result) => {
|
2022-06-05 20:04:25 +00:00
|
|
|
if (result) {
|
2018-06-22 05:15:04 +00:00
|
|
|
this.userRating = result.avg_rating;
|
|
|
|
}
|
|
|
|
return cb(err);
|
|
|
|
}
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
setHashTags(hashTags) {
|
2022-06-05 20:04:25 +00:00
|
|
|
if (_.isString(hashTags)) {
|
2018-06-22 05:15:04 +00:00
|
|
|
this.hashTags = new Set(hashTags.split(/[\s,]+/));
|
2022-06-05 20:04:25 +00:00
|
|
|
} else if (Array.isArray(hashTags)) {
|
2018-06-22 05:15:04 +00:00
|
|
|
this.hashTags = new Set(hashTags);
|
2022-06-05 20:04:25 +00:00
|
|
|
} else if (hashTags instanceof Set) {
|
2018-06-22 05:15:04 +00:00
|
|
|
this.hashTags = hashTags;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-06-05 20:04:25 +00:00
|
|
|
static get WellKnownMetaValues() {
|
2018-06-22 05:15:04 +00:00
|
|
|
return Object.keys(FILE_WELL_KNOWN_META);
|
|
|
|
}
|
|
|
|
|
2022-09-15 04:48:56 +00:00
|
|
|
static getFileIdsBySha(sha, options = {}, cb) {
|
2018-06-23 03:26:46 +00:00
|
|
|
// full or partial SHA-256
|
2022-09-15 04:48:56 +00:00
|
|
|
const limit = _.isNumber(options.limit) ? `LIMIT ${options.limit}` : '';
|
2018-06-22 05:15:04 +00:00
|
|
|
fileDb.all(
|
|
|
|
`SELECT file_id
|
2018-06-23 03:26:46 +00:00
|
|
|
FROM file
|
2022-09-15 04:48:56 +00:00
|
|
|
WHERE file_sha256 LIKE "${sha}%" ${limit};`,
|
2018-06-22 05:15:04 +00:00
|
|
|
(err, fileIdRows) => {
|
2022-06-05 20:04:25 +00:00
|
|
|
if (err) {
|
2018-06-22 05:15:04 +00:00
|
|
|
return cb(err);
|
|
|
|
}
|
|
|
|
|
2022-09-15 04:48:56 +00:00
|
|
|
return cb(
|
|
|
|
null,
|
|
|
|
(fileIdRows || []).map(r => r.file_id)
|
|
|
|
);
|
|
|
|
}
|
|
|
|
);
|
|
|
|
}
|
2018-06-22 05:15:04 +00:00
|
|
|
|
2022-09-15 04:48:56 +00:00
|
|
|
static findBySha(sha, cb) {
|
|
|
|
FileEntry.getFileIdsBySha(sha, { limit: 2 }, (err, fileIds) => {
|
|
|
|
if (err) {
|
|
|
|
return cb(err);
|
|
|
|
}
|
2018-06-22 05:15:04 +00:00
|
|
|
|
2022-09-15 04:48:56 +00:00
|
|
|
if (!fileIds || 0 === fileIds.length) {
|
|
|
|
return cb(Errors.DoesNotExist('No matches'));
|
2018-06-22 05:15:04 +00:00
|
|
|
}
|
2022-09-15 04:48:56 +00:00
|
|
|
|
|
|
|
if (fileIds.length > 1) {
|
|
|
|
return cb(Errors.Invalid('SHA is ambiguous'));
|
|
|
|
}
|
|
|
|
|
|
|
|
const fileEntry = new FileEntry();
|
|
|
|
return fileEntry.load(fileIds[0], err => {
|
|
|
|
return cb(err, fileEntry);
|
|
|
|
});
|
|
|
|
});
|
2018-06-22 05:15:04 +00:00
|
|
|
}
|
|
|
|
|
2018-11-04 22:01:27 +00:00
|
|
|
// Attempt to fine a file by an *existing* full path.
|
|
|
|
// Checkums may have changed and are not validated here.
|
|
|
|
static findByFullPath(fullPath, cb) {
|
|
|
|
// first, basic by-filename lookup.
|
2018-12-09 09:20:50 +00:00
|
|
|
FileEntry.findByFileNameWildcard(paths.basename(fullPath), (err, entries) => {
|
2022-06-05 20:04:25 +00:00
|
|
|
if (err) {
|
2018-11-04 22:01:27 +00:00
|
|
|
return cb(err);
|
|
|
|
}
|
2022-06-05 20:04:25 +00:00
|
|
|
if (!entries || !entries.length || entries.length > 1) {
|
2018-11-04 22:01:27 +00:00
|
|
|
return cb(Errors.DoesNotExist('No matches'));
|
|
|
|
}
|
|
|
|
|
|
|
|
// ensure the *full* path has not changed
|
|
|
|
// :TODO: if FS is case-insensitive, we probably want a better check here
|
|
|
|
const possibleMatch = entries[0];
|
2022-06-05 20:04:25 +00:00
|
|
|
if (possibleMatch.fullPath === fullPath) {
|
2018-11-04 22:01:27 +00:00
|
|
|
return cb(null, possibleMatch);
|
|
|
|
}
|
|
|
|
|
|
|
|
return cb(Errors.DoesNotExist('No matches'));
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2018-06-22 05:15:04 +00:00
|
|
|
static findByFileNameWildcard(wc, cb) {
|
2018-06-23 03:26:46 +00:00
|
|
|
// convert any * -> % and ? -> _ for SQLite syntax - see https://www.sqlite.org/lang_expr.html
|
2018-06-22 05:15:04 +00:00
|
|
|
wc = wc.replace(/\*/g, '%').replace(/\?/g, '_');
|
|
|
|
|
|
|
|
fileDb.all(
|
|
|
|
`SELECT file_id
|
2018-06-23 03:26:46 +00:00
|
|
|
FROM file
|
|
|
|
WHERE file_name LIKE "${wc}"
|
|
|
|
`,
|
2018-06-22 05:15:04 +00:00
|
|
|
(err, fileIdRows) => {
|
2022-06-05 20:04:25 +00:00
|
|
|
if (err) {
|
2018-06-22 05:15:04 +00:00
|
|
|
return cb(err);
|
|
|
|
}
|
|
|
|
|
2022-06-05 20:04:25 +00:00
|
|
|
if (!fileIdRows || 0 === fileIdRows.length) {
|
2018-06-22 05:15:04 +00:00
|
|
|
return cb(Errors.DoesNotExist('No matches'));
|
|
|
|
}
|
|
|
|
|
|
|
|
const entries = [];
|
2022-06-05 20:04:25 +00:00
|
|
|
async.each(
|
|
|
|
fileIdRows,
|
|
|
|
(row, nextRow) => {
|
|
|
|
const fileEntry = new FileEntry();
|
|
|
|
fileEntry.load(row.file_id, err => {
|
|
|
|
if (!err) {
|
|
|
|
entries.push(fileEntry);
|
|
|
|
}
|
|
|
|
return nextRow(err);
|
|
|
|
});
|
|
|
|
},
|
|
|
|
err => {
|
|
|
|
return cb(err, entries);
|
|
|
|
}
|
|
|
|
);
|
2018-06-22 05:15:04 +00:00
|
|
|
}
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2020-11-29 18:45:36 +00:00
|
|
|
//
|
|
|
|
// Find file(s) by |filter|
|
|
|
|
//
|
|
|
|
// - sort: sort results by any well known name, file_id, or user_rating
|
|
|
|
// - terms: one or more search terms to search within filenames as well
|
|
|
|
// as short and long descriptions. We attempt to use the FTS ability when
|
|
|
|
// possible, but want to allow users to search for wildcard matches in
|
|
|
|
// which some cases we'll use multiple LIKE queries.
|
|
|
|
// See _normalizeFileSearchTerms()
|
|
|
|
//
|
2018-06-22 05:15:04 +00:00
|
|
|
static findFiles(filter, cb) {
|
|
|
|
filter = filter || {};
|
|
|
|
|
|
|
|
let sql;
|
|
|
|
let sqlWhere = '';
|
|
|
|
let sqlOrderBy;
|
|
|
|
const sqlOrderDir = 'ascending' === filter.order ? 'ASC' : 'DESC';
|
|
|
|
|
2022-06-05 20:04:25 +00:00
|
|
|
if (moment.isMoment(filter.newerThanTimestamp)) {
|
2018-06-22 05:15:04 +00:00
|
|
|
filter.newerThanTimestamp = getISOTimestampString(filter.newerThanTimestamp);
|
|
|
|
}
|
|
|
|
|
|
|
|
function getOrderByWithCast(ob) {
|
2022-06-05 20:04:25 +00:00
|
|
|
if (['dl_count', 'est_release_year', 'byte_size'].indexOf(filter.sort) > -1) {
|
2018-06-22 05:15:04 +00:00
|
|
|
return `ORDER BY CAST(${ob} AS INTEGER)`;
|
|
|
|
}
|
|
|
|
|
|
|
|
return `ORDER BY ${ob}`;
|
|
|
|
}
|
|
|
|
|
|
|
|
function appendWhereClause(clause) {
|
2022-06-05 20:04:25 +00:00
|
|
|
if (sqlWhere) {
|
2018-06-22 05:15:04 +00:00
|
|
|
sqlWhere += ' AND ';
|
|
|
|
} else {
|
|
|
|
sqlWhere += ' WHERE ';
|
|
|
|
}
|
|
|
|
sqlWhere += clause;
|
|
|
|
}
|
|
|
|
|
2022-06-05 20:04:25 +00:00
|
|
|
if (filter.sort && filter.sort.length > 0) {
|
|
|
|
if (Object.keys(FILE_WELL_KNOWN_META).indexOf(filter.sort) > -1) {
|
|
|
|
// sorting via a meta value?
|
|
|
|
sql = `SELECT DISTINCT f.file_id
|
2018-06-23 03:26:46 +00:00
|
|
|
FROM file f, file_meta m`;
|
2017-01-19 05:23:53 +00:00
|
|
|
|
2022-06-05 20:04:25 +00:00
|
|
|
appendWhereClause(
|
|
|
|
`f.file_id = m.file_id AND m.meta_name = "${filter.sort}"`
|
|
|
|
);
|
2017-01-19 05:23:53 +00:00
|
|
|
|
2018-06-22 05:15:04 +00:00
|
|
|
sqlOrderBy = `${getOrderByWithCast('m.meta_value')} ${sqlOrderDir}`;
|
|
|
|
} else {
|
2018-06-23 03:26:46 +00:00
|
|
|
// additional special treatment for user ratings: we need to average them
|
2022-06-05 20:04:25 +00:00
|
|
|
if ('user_rating' === filter.sort) {
|
|
|
|
sql = `SELECT DISTINCT f.file_id,
|
2020-11-29 18:45:36 +00:00
|
|
|
(SELECT IFNULL(AVG(rating), 0) rating
|
|
|
|
FROM file_user_rating
|
2018-06-23 03:26:46 +00:00
|
|
|
WHERE file_id = f.file_id)
|
|
|
|
AS avg_rating
|
|
|
|
FROM file f`;
|
2018-01-12 04:17:26 +00:00
|
|
|
|
2018-06-22 05:15:04 +00:00
|
|
|
sqlOrderBy = `ORDER BY avg_rating ${sqlOrderDir}`;
|
|
|
|
} else {
|
2022-06-05 20:04:25 +00:00
|
|
|
sql = `SELECT DISTINCT f.file_id
|
2018-06-23 03:26:46 +00:00
|
|
|
FROM file f`;
|
2017-02-08 03:20:10 +00:00
|
|
|
|
2022-06-05 20:04:25 +00:00
|
|
|
sqlOrderBy =
|
|
|
|
getOrderByWithCast(`f.${filter.sort}`) + ' ' + sqlOrderDir;
|
2018-06-22 05:15:04 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
2022-06-05 20:04:25 +00:00
|
|
|
sql = `SELECT DISTINCT f.file_id
|
2018-06-23 03:26:46 +00:00
|
|
|
FROM file f`;
|
2017-01-19 05:23:53 +00:00
|
|
|
|
2018-06-22 05:15:04 +00:00
|
|
|
sqlOrderBy = `${getOrderByWithCast('f.file_id')} ${sqlOrderDir}`;
|
|
|
|
}
|
|
|
|
|
2022-06-05 20:04:25 +00:00
|
|
|
if (filter.areaTag && filter.areaTag.length > 0) {
|
|
|
|
if (Array.isArray(filter.areaTag)) {
|
2018-06-22 05:15:04 +00:00
|
|
|
const areaList = filter.areaTag.map(t => `"${t}"`).join(', ');
|
|
|
|
appendWhereClause(`f.area_tag IN(${areaList})`);
|
|
|
|
} else {
|
|
|
|
appendWhereClause(`f.area_tag = "${filter.areaTag}"`);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-06-05 20:04:25 +00:00
|
|
|
if (filter.metaPairs && filter.metaPairs.length > 0) {
|
2018-06-22 05:15:04 +00:00
|
|
|
filter.metaPairs.forEach(mp => {
|
2022-06-05 20:04:25 +00:00
|
|
|
if (mp.wildcards) {
|
2018-06-23 03:26:46 +00:00
|
|
|
// convert any * -> % and ? -> _ for SQLite syntax - see https://www.sqlite.org/lang_expr.html
|
2018-06-22 05:15:04 +00:00
|
|
|
mp.value = mp.value.replace(/\*/g, '%').replace(/\?/g, '_');
|
|
|
|
appendWhereClause(
|
|
|
|
`f.file_id IN (
|
2020-11-29 18:45:36 +00:00
|
|
|
SELECT file_id
|
|
|
|
FROM file_meta
|
2018-06-23 03:26:46 +00:00
|
|
|
WHERE meta_name = "${mp.name}" AND meta_value LIKE "${mp.value}"
|
|
|
|
)`
|
2018-06-22 05:15:04 +00:00
|
|
|
);
|
|
|
|
} else {
|
|
|
|
appendWhereClause(
|
|
|
|
`f.file_id IN (
|
2020-11-29 18:45:36 +00:00
|
|
|
SELECT file_id
|
|
|
|
FROM file_meta
|
2018-06-23 03:26:46 +00:00
|
|
|
WHERE meta_name = "${mp.name}" AND meta_value = "${mp.value}"
|
|
|
|
)`
|
2018-06-22 05:15:04 +00:00
|
|
|
);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2022-06-05 20:04:25 +00:00
|
|
|
if (filter.storageTag && filter.storageTag.length > 0) {
|
2018-06-22 05:15:04 +00:00
|
|
|
appendWhereClause(`f.storage_tag="${filter.storageTag}"`);
|
|
|
|
}
|
|
|
|
|
2022-06-05 20:04:25 +00:00
|
|
|
if (filter.terms && filter.terms.length > 0) {
|
2020-11-29 18:45:36 +00:00
|
|
|
const [terms, queryType] = FileEntry._normalizeFileSearchTerms(filter.terms);
|
|
|
|
|
|
|
|
if ('fts_match' === queryType) {
|
|
|
|
// note the ':' in MATCH expr., see https://www.sqlite.org/cvstrac/wiki?p=FullTextIndex
|
|
|
|
appendWhereClause(
|
|
|
|
`f.file_id IN (
|
|
|
|
SELECT rowid
|
|
|
|
FROM file_fts
|
|
|
|
WHERE file_fts MATCH ":${terms}"
|
|
|
|
)`
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
appendWhereClause(
|
|
|
|
`(f.file_name LIKE "${terms}" OR
|
|
|
|
f.desc LIKE "${terms}" OR
|
|
|
|
f.desc_long LIKE "${terms}")`
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// handle e.g. 1998 -> "1998"
|
|
|
|
if (_.isNumber(filter.tags)) {
|
|
|
|
filter.tags = filter.tags.toString();
|
2018-06-22 05:15:04 +00:00
|
|
|
}
|
2018-01-15 19:22:11 +00:00
|
|
|
|
2022-06-05 20:04:25 +00:00
|
|
|
if (filter.tags && filter.tags.length > 0) {
|
2018-06-23 03:26:46 +00:00
|
|
|
// build list of quoted tags; filter.tags comes in as a space and/or comma separated values
|
2022-06-05 20:04:25 +00:00
|
|
|
const tags = filter.tags
|
|
|
|
.replace(/,/g, ' ')
|
|
|
|
.replace(/\s{2,}/g, ' ')
|
|
|
|
.split(' ')
|
|
|
|
.map(tag => `"${sanitizeString(tag)}"`)
|
|
|
|
.join(',');
|
2016-12-07 01:58:56 +00:00
|
|
|
|
2018-06-22 05:15:04 +00:00
|
|
|
appendWhereClause(
|
|
|
|
`f.file_id IN (
|
2018-06-23 03:26:46 +00:00
|
|
|
SELECT file_id
|
|
|
|
FROM file_hash_tag
|
|
|
|
WHERE hash_tag_id IN (
|
|
|
|
SELECT hash_tag_id
|
|
|
|
FROM hash_tag
|
|
|
|
WHERE hash_tag IN (${tags})
|
|
|
|
)
|
|
|
|
)`
|
2018-06-22 05:15:04 +00:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2022-06-05 20:04:25 +00:00
|
|
|
if (
|
|
|
|
_.isString(filter.newerThanTimestamp) &&
|
|
|
|
filter.newerThanTimestamp.length > 0
|
|
|
|
) {
|
|
|
|
appendWhereClause(
|
|
|
|
`DATETIME(f.upload_timestamp) > DATETIME("${filter.newerThanTimestamp}", "+1 seconds")`
|
|
|
|
);
|
2018-06-22 05:15:04 +00:00
|
|
|
}
|
|
|
|
|
2022-06-05 20:04:25 +00:00
|
|
|
if (_.isNumber(filter.newerThanFileId)) {
|
2018-06-22 05:15:04 +00:00
|
|
|
appendWhereClause(`f.file_id > ${filter.newerThanFileId}`);
|
|
|
|
}
|
|
|
|
|
|
|
|
sql += `${sqlWhere} ${sqlOrderBy}`;
|
|
|
|
|
2022-06-05 20:04:25 +00:00
|
|
|
if (_.isNumber(filter.limit)) {
|
2018-06-22 05:15:04 +00:00
|
|
|
sql += ` LIMIT ${filter.limit}`;
|
|
|
|
}
|
|
|
|
|
|
|
|
sql += ';';
|
|
|
|
|
|
|
|
fileDb.all(sql, (err, rows) => {
|
2022-06-05 20:04:25 +00:00
|
|
|
if (err) {
|
2018-06-22 05:15:04 +00:00
|
|
|
return cb(err);
|
|
|
|
}
|
2022-06-05 20:04:25 +00:00
|
|
|
if (!rows || 0 === rows.length) {
|
|
|
|
return cb(null, []); // no matches
|
2018-06-22 05:15:04 +00:00
|
|
|
}
|
2022-06-05 20:04:25 +00:00
|
|
|
return cb(
|
|
|
|
null,
|
|
|
|
rows.map(r => r.file_id)
|
|
|
|
);
|
2018-06-22 05:15:04 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
static removeEntry(srcFileEntry, options, cb) {
|
2022-06-05 20:04:25 +00:00
|
|
|
if (!_.isFunction(cb) && _.isFunction(options)) {
|
2018-06-22 05:15:04 +00:00
|
|
|
cb = options;
|
|
|
|
options = {};
|
|
|
|
}
|
|
|
|
|
|
|
|
async.series(
|
|
|
|
[
|
|
|
|
function removeFromDatabase(callback) {
|
|
|
|
fileDb.run(
|
|
|
|
`DELETE FROM file
|
2018-06-23 03:26:46 +00:00
|
|
|
WHERE file_id = ?;`,
|
2022-06-05 20:04:25 +00:00
|
|
|
[srcFileEntry.fileId],
|
2018-06-22 05:15:04 +00:00
|
|
|
err => {
|
|
|
|
return callback(err);
|
|
|
|
}
|
|
|
|
);
|
|
|
|
},
|
|
|
|
function optionallyRemovePhysicalFile(callback) {
|
2022-06-05 20:04:25 +00:00
|
|
|
if (true !== options.removePhysFile) {
|
2018-06-22 05:15:04 +00:00
|
|
|
return callback(null);
|
|
|
|
}
|
|
|
|
|
|
|
|
unlink(srcFileEntry.filePath, err => {
|
|
|
|
return callback(err);
|
|
|
|
});
|
2022-06-05 20:04:25 +00:00
|
|
|
},
|
2018-06-22 05:15:04 +00:00
|
|
|
],
|
|
|
|
err => {
|
|
|
|
return cb(err);
|
|
|
|
}
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
static moveEntry(srcFileEntry, destAreaTag, destStorageTag, destFileName, cb) {
|
2022-06-05 20:04:25 +00:00
|
|
|
if (!cb && _.isFunction(destFileName)) {
|
2018-06-22 05:15:04 +00:00
|
|
|
cb = destFileName;
|
|
|
|
destFileName = srcFileEntry.fileName;
|
|
|
|
}
|
|
|
|
|
2022-06-05 20:04:25 +00:00
|
|
|
const srcPath = srcFileEntry.filePath;
|
|
|
|
const dstDir = FileEntry.getAreaStorageDirectoryByTag(destStorageTag);
|
2018-06-22 05:15:04 +00:00
|
|
|
|
2022-06-05 20:04:25 +00:00
|
|
|
if (!dstDir) {
|
2018-06-22 05:15:04 +00:00
|
|
|
return cb(Errors.Invalid('Invalid storage tag'));
|
|
|
|
}
|
|
|
|
|
2022-06-05 20:04:25 +00:00
|
|
|
const dstPath = paths.join(dstDir, destFileName);
|
2018-06-22 05:15:04 +00:00
|
|
|
|
|
|
|
async.series(
|
|
|
|
[
|
|
|
|
function movePhysFile(callback) {
|
2022-06-05 20:04:25 +00:00
|
|
|
if (srcPath === dstPath) {
|
|
|
|
return callback(null); // don't need to move file, but may change areas
|
2018-06-22 05:15:04 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
fse.move(srcPath, dstPath, err => {
|
|
|
|
return callback(err);
|
|
|
|
});
|
|
|
|
},
|
|
|
|
function updateDatabase(callback) {
|
|
|
|
fileDb.run(
|
|
|
|
`UPDATE file
|
2018-06-23 03:26:46 +00:00
|
|
|
SET area_tag = ?, file_name = ?, storage_tag = ?
|
|
|
|
WHERE file_id = ?;`,
|
2022-06-05 20:04:25 +00:00
|
|
|
[destAreaTag, destFileName, destStorageTag, srcFileEntry.fileId],
|
2018-06-22 05:15:04 +00:00
|
|
|
err => {
|
|
|
|
return callback(err);
|
|
|
|
}
|
|
|
|
);
|
2022-06-05 20:04:25 +00:00
|
|
|
},
|
2018-06-22 05:15:04 +00:00
|
|
|
],
|
|
|
|
err => {
|
|
|
|
return cb(err);
|
|
|
|
}
|
|
|
|
);
|
|
|
|
}
|
2020-11-29 18:45:36 +00:00
|
|
|
|
|
|
|
static _normalizeFileSearchTerms(terms) {
|
|
|
|
// ensure we have reasonable input to start with
|
|
|
|
terms = sanitizeString(terms.toString());
|
|
|
|
|
|
|
|
// No wildcards?
|
|
|
|
const hasSingleCharWC = terms.indexOf('?') > -1;
|
|
|
|
if (terms.indexOf('*') === -1 && !hasSingleCharWC) {
|
2022-06-05 20:04:25 +00:00
|
|
|
return [terms, 'fts_match'];
|
2020-11-29 18:45:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
const prepareLike = () => {
|
|
|
|
// Convert * and ? to SQL LIKE style
|
|
|
|
terms = terms.replace(/\*/g, '%').replace(/\?/g, '_');
|
|
|
|
return terms;
|
|
|
|
};
|
|
|
|
|
|
|
|
// Any ? wildcards?
|
|
|
|
if (hasSingleCharWC) {
|
2022-06-05 20:04:25 +00:00
|
|
|
return [prepareLike(terms), 'like'];
|
2020-11-29 18:45:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
const split = terms.replace(/\s+/g, ' ').split(' ');
|
|
|
|
const useLike = split.some(term => {
|
|
|
|
if (term.indexOf('?') > -1) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
const wcPos = term.indexOf('*');
|
|
|
|
if (wcPos > -1 && wcPos !== term.length - 1) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
});
|
|
|
|
|
|
|
|
if (useLike) {
|
2022-06-05 20:04:25 +00:00
|
|
|
return [prepareLike(terms), 'like'];
|
2020-11-29 18:45:36 +00:00
|
|
|
}
|
|
|
|
|
2022-06-05 20:04:25 +00:00
|
|
|
return [terms, 'fts_match'];
|
2020-11-29 18:45:36 +00:00
|
|
|
}
|
2016-09-29 03:54:25 +00:00
|
|
|
};
|