mediaService.js 12 KB

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