mediaService.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277
  1. const path = require('path');
  2. const fs = require('fs');
  3. const MediaFileModel = require('./mediaItem.js').MediaFileModel;
  4. const { MediaFileTagModel } = require('./mediaItemTag.js');
  5. const { MediaFileMetaModel } = require('./mediaItemMeta.js');
  6. const { AccessModel, ACCESS_TYPE, ACCESS_TO, ACCESS_GRANT } = require('./access.js');
  7. function Media()
  8. {
  9. }
  10. function MediaStruct(i) {
  11. this.path = i.path;
  12. this.fileName = path.parse(i.path).name;
  13. this.md5sum = i.md5sum;
  14. this.fixedSum = i.fixedSum;
  15. this.date = i.date;
  16. this.meta = {};
  17. this.tags = [];
  18. this.fixedTags = [];
  19. this.accessType = -1;
  20. this.version = i.version;
  21. }
  22. MediaStruct.prototype.pushMeta = function(key, value) {
  23. if (key && value && !this.meta[key]) {
  24. let type = '';
  25. let roTypes = [
  26. 'photochamberImport', 'height', 'width', 'iso', 'focal',
  27. 'fNumber', 'exposureTime', 'camera', 'lensModel',
  28. 'exposureTimeStr', 'libraryPath', 'compression',
  29. 'software', 'fileSize', 'geoHash', 'exposureProgram',
  30. 'orientation'
  31. ];
  32. if (['photochamberImport', 'dateTime'].indexOf(key) >= 0)
  33. type = 'date';
  34. else if (['height', 'width', 'iso', 'focal', 'fNumber', 'exposureTime', 'orientation'].indexOf(key) >= 0)
  35. type = 'number';
  36. else if (['geoHash', 'gpsLocation'].indexOf(key) >= 0)
  37. type = 'geoData';
  38. else if (['artist', 'camera', 'lensModel', 'exposureTimeStr', 'libraryPath', 'compression',
  39. 'software', 'geoCountry', 'geoAdmin', 'geoCity', 'exposureProgram'].indexOf(key) >= 0)
  40. type = 'string';
  41. else if (['fileSize'].indexOf(key) >= 0)
  42. type = 'octet';
  43. else console.log(`Unknown meta type ${key} (${value})`);
  44. this.meta[key] = {
  45. type: type,
  46. canWrite: roTypes.indexOf(key) === -1,
  47. value: value
  48. };
  49. }
  50. }
  51. MediaStruct.prototype.pushTag = function(tag, isFixedTag) {
  52. if (!tag)
  53. return;
  54. if (!isFixedTag && this.tags.indexOf(tag) === -1)
  55. this.tags.push(tag);
  56. if (isFixedTag && this.fixedTags.indexOf(tag) === -1)
  57. this.fixedTags.push(tag);
  58. }
  59. MediaStruct.prototype.computeAccess = function(accessList) {
  60. if (this.accessType > -1)
  61. return this.accessType;
  62. if (!fs.existsSync(this.path))
  63. return this.accessType = ACCESS_GRANT.none;
  64. const checkTag = function(tags, access) {
  65. if (!tags.length)
  66. return false;
  67. for (let i of tags)
  68. if (i.startsWith(access.accessToData+'/') || i === access.accessToData)
  69. return true;
  70. return false;
  71. }
  72. const checkMeta = function(metas, access) {
  73. if (!access.accessToDataDeserialized)
  74. return false;
  75. let metaKey = Object.keys(access.accessToDataDeserialized)[0];
  76. let meta = metas[metaKey]?.value;
  77. return meta && metaKey && meta == access.accessToDataDeserialized[metaKey];
  78. }
  79. this.accessType = ACCESS_GRANT.none;
  80. for (let i of accessList) {
  81. if (i.accessTo === ACCESS_TO.everything ||
  82. (i.accessTo === ACCESS_TO.item && i.accessToData === this.fixedSum) ||
  83. (i.accessTo === ACCESS_TO.meta && checkMeta(this.meta, i)) ||
  84. (i.accessTo === ACCESS_TO.tag && checkTag([].concat(this.fixedTags, this.tags), i))) {
  85. if (i.grant === ACCESS_GRANT.write)
  86. return this.accessType = ACCESS_GRANT.write;
  87. if (i.grant === ACCESS_GRANT.read ||
  88. (i.grant === ACCESS_GRANT.readNoMeta && this.accessType === ACCESS_GRANT.none))
  89. this.accessType = i.grant;
  90. }
  91. }
  92. return this.accessType;
  93. }
  94. MediaStruct.prototype.HaveAccess = function(accessList) {
  95. return this.computeAccess(accessList) > 0;
  96. }
  97. async function buildAccessList(app, accessIds) {
  98. accessIds = Object.keys(accessIds || {}).reduce((acc, i) => {
  99. accessIds[i].linkId && acc.links.push(accessIds[i].linkId);
  100. accessIds[i].ldapDn && acc.ldap.push(accessIds[i].ldapDn);
  101. accessIds[i].email && acc.emails.push(accessIds[i].email);
  102. return acc;
  103. }, {links:[], emails: [], ldap: []});
  104. accessIds.accData = [].concat(accessIds.ldap, accessIds.emails, accessIds.links);
  105. accessIds.links = accessIds.links.map(x => '?').join(',');
  106. accessIds.emails = accessIds.emails.map(x => '?').join(',');
  107. accessIds.ldap = accessIds.ldap.map(x => '?').join(',');
  108. let accessList = (await app.databaseHelper.runSql(`select * from access where (
  109. (type=${ACCESS_TYPE.ldapAccount} AND typeData in (${accessIds.ldap})) OR
  110. (type=${ACCESS_TYPE.email} AND typeData in (${accessIds.emails})) OR
  111. (type=${ACCESS_TYPE.link} AND typeData in (${accessIds.links})) OR
  112. type=${ACCESS_TYPE.everyOne}
  113. )`, accessIds.accData)).map(data => {
  114. let result = new AccessModel;
  115. result.fromDb(data);
  116. return result;
  117. });
  118. return (accessList || []).filter(i => i.grant !== ACCESS_GRANT.none);
  119. }
  120. function reduceReqToMediaStruct(acc, i) {
  121. let obj = acc[i.fixedSum] = acc[i.fixedSum] || new MediaStruct(i);
  122. obj.pushMeta(i.metaKey, i.metaValue);
  123. obj.pushTag(i.mediaTag, i.isFixedTag);
  124. return acc;
  125. }
  126. module.exports.fetchMultiple = async function(app, md5sums, accessList, maxVersion) {
  127. let result = ((await app.databaseHelper.runSql(`
  128. select mediaFile.path, mediaFile.md5sum, mediaFile.date, mediaFile.fixedSum,
  129. mediaMeta.key as metaKey, mediaMeta.value as metaValue,
  130. mediaTag.tag as mediaTag, (mediaTag.fromMeta or mediaTag.fromAutotag) as isFixedTag,
  131. mediaFile.version
  132. from mediaFile
  133. left join mediaMeta on mediaMeta.md5sum=mediaFile.fixedSum
  134. left join mediaTag on mediaTag.md5sum=mediaFile.fixedSum
  135. where mediaFile.inaccessible=0 and mediaFile.version>? and mediaFile.fixedSum in (` +md5sums.map(x => '?').join(',')+`)`, [maxVersion].concat(md5sums))) || []).reduce(reduceReqToMediaStruct, {}) || null;
  136. accessList = await buildAccessList(app, accessList);
  137. for (let key in result)
  138. if (!result[key].HaveAccess(accessList))
  139. delete result[key];
  140. return result;
  141. }
  142. module.exports.fetchOne = async function(app, md5sum, accessList, maxVersion) {
  143. return (await module.exports.fetchMultiple(app, [md5sum], accessList, maxVersion))[md5sum] || null;
  144. }
  145. function fetchAccessSubQuery(args, access) {
  146. let result = [];
  147. if (access.filter(i => i.accessTo === ACCESS_TO.everything).length) return "1=1";
  148. for (let i of access) {
  149. if (i.accessTo === ACCESS_TO.item) {
  150. result.push(`fixedSum=?`);
  151. args.push(i.accessToData);
  152. }
  153. else if (i.accessTo === ACCESS_TO.meta && i.accessToDataDeserialized) {
  154. result.push('(mediaMeta.key=? and mediaMeta.value=?)');
  155. const metaKey = Object.keys(i.accessToDataDeserialized)[0];
  156. args.push(metaKey);
  157. args.push(i.accessToDataDeserialized[metaKey]);
  158. }
  159. else if (i.accessTo === ACCESS_TO.tag && i.accessToData) {
  160. result.push('(mediaTag.tag=? or mediaTag.tag like ?)');
  161. args.push(i.accessToData);
  162. args.push(`${i.accessToData}/%`);
  163. }
  164. }
  165. return result.join(" or ") || "1=0";
  166. }
  167. function fetchMediasBuildSubQuery(startTs, count, access, maxVersion) {
  168. let query = "select distinct fixedSum from mediaFile";
  169. let whereClause = ` where inaccessible=0 and version>?`;
  170. let accessWhere = null;
  171. let args = [maxVersion];
  172. // access filter
  173. if (access !== undefined) {
  174. accessWhere = fetchAccessSubQuery(args, access);
  175. // join
  176. if (accessWhere)
  177. query += ` left join mediaMeta on mediaMeta.md5sum=mediaFile.fixedSum` +
  178. ` left join mediaTag on mediaTag.md5sum=mediaFile.fixedSum`;
  179. if (accessWhere)
  180. whereClause += ` and (${accessWhere})`;
  181. }
  182. // date filter
  183. if (startTs) {
  184. whereClause += ' and date <?'
  185. args.push(startTs);
  186. }
  187. // order and limit
  188. query += whereClause + " order by date desc ";
  189. if (count > 0) {
  190. query += "limit ?";
  191. args.push(count);
  192. }
  193. return {
  194. query: query,
  195. args: args
  196. };
  197. }
  198. module.exports.removeMedia = async function(app, media) {
  199. await app.databaseHelper.remove(MediaFileTagModel, { md5sum: media.fixedSum });
  200. await app.databaseHelper.remove(MediaFileMetaModel, { md5sum: media.fixedSum });
  201. await app.databaseHelper.remove(MediaFileModel, { fixedSum: media.fixedSum });
  202. await app.libraryManager.removeMedia(media.path);
  203. }
  204. module.exports.fetchMedias = async function(app, startTs, count, access, maxVersion) {
  205. let subQuery = fetchMediasBuildSubQuery(startTs, count, access, maxVersion);
  206. let result = ((await app.databaseHelper.runSql(`
  207. select mediaFile.path, mediaFile.md5sum, mediaFile.date, mediaFile.fixedSum,
  208. mediaMeta.key as metaKey, mediaMeta.value as metaValue,
  209. mediaTag.tag as mediaTag, (mediaTag.fromMeta or mediaTag.fromAutotag) as isFixedTag,
  210. mediaFile.version
  211. from mediaFile
  212. left join mediaMeta on mediaMeta.md5sum=mediaFile.fixedSum
  213. left join mediaTag on mediaTag.md5sum=mediaFile.fixedSum
  214. where mediaFile.fixedSum in (${subQuery.query})`, subQuery.args)) || [])
  215. .reduce(reduceReqToMediaStruct, {});
  216. result = Object.keys(result).map(i => result[i]).sort((a, b) => b.date-a.date);
  217. return result;
  218. };
  219. module.exports.fetchMediasSumWithAccess = async function(app, access) {
  220. const subQuery = fetchMediasBuildSubQuery(0, -1, await buildAccessList(app, access), 0);
  221. return (await app.databaseHelper.runSql(subQuery.query, subQuery.args)).map(x => x.fixedSum);
  222. };
  223. module.exports.fetchMediasWithAccess = async function(app, startTs, count, access, maxVersion) {
  224. let result = [];
  225. let lastTs = startTs;
  226. access = await buildAccessList(app, access);
  227. while (result.length < count) {
  228. let tmp = await module.exports.fetchMedias(app, lastTs, 25, access, maxVersion);
  229. if (!tmp.length)
  230. return result;
  231. lastTs = tmp[tmp.length-1].date;
  232. tmp = tmp.filter(i => i.HaveAccess(access));
  233. if (tmp.length)
  234. result = result.concat(tmp);
  235. }
  236. return result.slice(0, count);
  237. };
  238. module.exports.updateVersionInDb = function(app, fixedSum) {
  239. return app.databaseHelper.rawUpdate(MediaFileModel.prototype, { fixedSum: fixedSum }, { version: Date.now() });
  240. }
  241. module.exports.getMediaRange = async function(app) {
  242. let result = await app.databaseHelper.runSql("select min(date) as _min, max(date) as _max, max(version) as _version from mediaFile");
  243. return {
  244. min: result?.[0]?._min || 0,
  245. max: result?.[0]?._max || 0,
  246. maxVersion: result?.[0]?._version || 0
  247. };
  248. }