postgres.js 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188
  1. const { Blob } = require('overleaf-editor-core')
  2. const assert = require('../assert')
  3. const knex = require('../knex')
  4. /**
  5. * Set up the initial data structures for a project
  6. */
  7. async function initialize(projectId) {
  8. // Nothing to do for Postgres
  9. }
  10. /**
  11. * Copy the data structures for a given project.
  12. * @param {string} sourceProjectId
  13. * @param {string} targetProjectId
  14. */
  15. async function clone(sourceProjectId, targetProjectId) {
  16. assert.postgresId(targetProjectId, 'bad target projectId')
  17. assert.postgresId(sourceProjectId, 'bad source projectId')
  18. const result = await knex.raw(
  19. `INSERT INTO project_blobs (
  20. project_id, hash_bytes, byte_length, string_length
  21. )
  22. SELECT ?, hash_bytes, byte_length, string_length
  23. FROM project_blobs
  24. WHERE project_id = ?
  25. RETURNING hash_bytes`,
  26. [parseInt(targetProjectId, 10), parseInt(sourceProjectId, 10)]
  27. )
  28. const blobHashes = []
  29. for (const row of result.rows) {
  30. blobHashes.push(row.hash_bytes.toString('hex'))
  31. }
  32. return blobHashes
  33. }
  34. /**
  35. * Return blob metadata for the given project and hash
  36. */
  37. async function findBlob(projectId, hash) {
  38. assert.postgresId(projectId, 'bad projectId')
  39. projectId = parseInt(projectId, 10)
  40. assert.blobHash(hash, 'bad hash')
  41. const binaryHash = hashToBuffer(hash)
  42. const record = await knex('project_blobs')
  43. .select('hash_bytes', 'byte_length', 'string_length')
  44. .where({
  45. project_id: projectId,
  46. hash_bytes: binaryHash,
  47. })
  48. .first()
  49. return recordToBlob(record)
  50. }
  51. /**
  52. * Read multiple blob metadata records by hexadecimal hashes.
  53. *
  54. * @param {Array.<string>} hashes hexadecimal SHA-1 hashes
  55. * @return {Promise.<Array.<Blob?>>} no guarantee on order
  56. */
  57. async function findBlobs(projectId, hashes) {
  58. assert.postgresId(projectId, 'bad projectId')
  59. projectId = parseInt(projectId, 10)
  60. assert.array(hashes, 'bad hashes: not array')
  61. hashes.forEach(function (hash) {
  62. assert.blobHash(hash, 'bad hash')
  63. })
  64. const binaryHashes = hashes.map(hashToBuffer)
  65. const records = await knex('project_blobs')
  66. .select('hash_bytes', 'byte_length', 'string_length')
  67. .where('project_id', projectId)
  68. .whereIn('hash_bytes', binaryHashes)
  69. const blobs = records.map(recordToBlob)
  70. return blobs
  71. }
  72. /**
  73. * Return metadata for all blobs in the given project
  74. */
  75. async function getProjectBlobs(projectId) {
  76. assert.postgresId(projectId, 'bad projectId')
  77. projectId = parseInt(projectId, 10)
  78. const records = await knex('project_blobs')
  79. .select('hash_bytes', 'byte_length', 'string_length')
  80. .where({
  81. project_id: projectId,
  82. })
  83. const blobs = records.map(recordToBlob)
  84. return blobs
  85. }
  86. /**
  87. * Return metadata for all blobs in the given project
  88. * @param {Array<number>} projectIds
  89. * @return {Promise<{ nBlobs: number, blobs: Map<number, Array<Blob>> }>}
  90. */
  91. async function getProjectBlobsBatch(projectIds) {
  92. for (const projectId of projectIds) {
  93. assert.integer(projectId, 'bad projectId')
  94. }
  95. let nBlobs = 0
  96. const blobs = new Map()
  97. if (projectIds.length === 0) return { nBlobs, blobs }
  98. const cursor = knex('project_blobs')
  99. .select('project_id', 'hash_bytes', 'byte_length', 'string_length')
  100. .whereIn('project_id', projectIds)
  101. .stream()
  102. for await (const record of cursor) {
  103. const found = blobs.get(record.project_id)
  104. if (found) {
  105. found.push(recordToBlob(record))
  106. } else {
  107. blobs.set(record.project_id, [recordToBlob(record)])
  108. }
  109. nBlobs++
  110. }
  111. return { nBlobs, blobs }
  112. }
  113. /**
  114. * Add a blob's metadata to the blobs table after it has been uploaded.
  115. */
  116. async function insertBlob(projectId, blob) {
  117. assert.postgresId(projectId, 'bad projectId')
  118. projectId = parseInt(projectId, 10)
  119. await knex('project_blobs')
  120. .insert(blobToRecord(projectId, blob))
  121. .onConflict(['project_id', 'hash_bytes'])
  122. .ignore()
  123. }
  124. /**
  125. * Deletes all blobs for a given project
  126. */
  127. async function deleteBlobs(projectId) {
  128. assert.postgresId(projectId, 'bad projectId')
  129. projectId = parseInt(projectId, 10)
  130. await knex('project_blobs').where('project_id', projectId).delete()
  131. }
  132. function blobToRecord(projectId, blob) {
  133. return {
  134. project_id: projectId,
  135. hash_bytes: hashToBuffer(blob.hash),
  136. byte_length: blob.getByteLength(),
  137. string_length: blob.getStringLength(),
  138. }
  139. }
  140. function recordToBlob(record) {
  141. if (!record) return
  142. return new Blob(
  143. hashFromBuffer(record.hash_bytes),
  144. record.byte_length,
  145. record.string_length
  146. )
  147. }
  148. function hashToBuffer(hash) {
  149. if (!hash) return
  150. return Buffer.from(hash, 'hex')
  151. }
  152. function hashFromBuffer(buffer) {
  153. if (!buffer) return
  154. return buffer.toString('hex')
  155. }
  156. module.exports = {
  157. initialize,
  158. clone,
  159. findBlob,
  160. findBlobs,
  161. getProjectBlobs,
  162. getProjectBlobsBatch,
  163. insertBlob,
  164. deleteBlobs,
  165. }