index.js 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201
  1. /*
  2. * Created by Wu Jian Ping on - 2022/07/22.
  3. */
  4. const fs = require('fs')
  5. const VectorIndexSize = 8
  6. const VectorIndexCols = 256
  7. const VectorIndexLength = 256 * 256 * (4 + 4)
  8. const SegmentIndexSize = 14
  9. const IP_REGEX = /^((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.){3}(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])$/
  10. const getStartEndPtr = Symbol('#getStartEndPtr')
  11. const getBuffer = Symbol('#getBuffer')
  12. const openFilePromise = Symbol('#openFilePromise')
  13. class Searcher {
  14. constructor (dbFile, vectorIndex, buffer) {
  15. this._dbFile = dbFile
  16. this._vectorIndex = vectorIndex
  17. this._buffer = buffer
  18. if (this._buffer) {
  19. this._vectorIndex = this._buffer.subarray(256, 256 + VectorIndexLength)
  20. }
  21. }
  22. async [getStartEndPtr] (idx, fd, ioStatus) {
  23. if (this._vectorIndex) {
  24. const sPtr = this._vectorIndex.readUInt32LE(idx)
  25. const ePtr = this._vectorIndex.readUInt32LE(idx + 4)
  26. return { sPtr, ePtr }
  27. } else {
  28. const buf = await this[getBuffer](256 + idx, 8, fd, ioStatus)
  29. const sPtr = buf.readUInt32LE()
  30. const ePtr = buf.readUInt32LE(4)
  31. return { sPtr, ePtr }
  32. }
  33. }
  34. async [getBuffer] (offset, length, fd, ioStatus) {
  35. if (this._buffer) {
  36. return this._buffer.subarray(offset, offset + length)
  37. } else {
  38. const buf = Buffer.alloc(length)
  39. return new Promise((resolve, reject) => {
  40. ioStatus.ioCount += 1
  41. fs.read(fd, buf, 0, length, offset, (err) => {
  42. if (err) {
  43. reject(err)
  44. } else {
  45. resolve(buf)
  46. }
  47. })
  48. })
  49. }
  50. }
  51. [openFilePromise] (fileName) {
  52. return new Promise((resolve, reject) => {
  53. fs.open(fileName, 'r', (err, fd) => {
  54. if (err) {
  55. reject(err)
  56. } else {
  57. resolve(fd)
  58. }
  59. })
  60. })
  61. }
  62. async search (ip) {
  63. const startTime = process.hrtime()
  64. const ioStatus = {
  65. ioCount: 0
  66. }
  67. if (!isValidIp(ip)) {
  68. throw new Error(`IP: ${ip} is invalid`)
  69. }
  70. let fd = null
  71. if (!this._buffer) {
  72. fd = await this[openFilePromise](this._dbFile)
  73. }
  74. const ps = ip.split('.')
  75. const i0 = parseInt(ps[0])
  76. const i1 = parseInt(ps[1])
  77. const i2 = parseInt(ps[2])
  78. const i3 = parseInt(ps[3])
  79. const ipInt = i0 * 256 * 256 * 256 + i1 * 256 * 256 + i2 * 256 + i3
  80. const idx = i0 * VectorIndexCols * VectorIndexSize + i1 * VectorIndexSize
  81. const { sPtr, ePtr } = await this[getStartEndPtr](idx, fd, ioStatus)
  82. let l = 0
  83. let h = (ePtr - sPtr) / SegmentIndexSize
  84. let result = null
  85. while (l <= h) {
  86. const m = (l + h) >> 1
  87. const p = sPtr + m * SegmentIndexSize
  88. const buff = await this[getBuffer](p, SegmentIndexSize, fd, ioStatus)
  89. const sip = buff.readUInt32LE(0)
  90. if (ipInt < sip) {
  91. h = m - 1
  92. } else {
  93. const eip = buff.readUInt32LE(4)
  94. if (ipInt > eip) {
  95. l = m + 1
  96. } else {
  97. const dataLen = buff.readUInt16LE(8)
  98. const dataPtr = buff.readUInt32LE(10)
  99. const data = await this[getBuffer](dataPtr, dataLen, fd, ioStatus)
  100. result = data.toString('utf-8')
  101. break
  102. }
  103. }
  104. }
  105. if (fd) {
  106. fs.close(fd,function(){})
  107. }
  108. const diff = process.hrtime(startTime)
  109. const took = (diff[0] * 1e9 + diff[1]) / 1e3
  110. return { region: result, ioCount: ioStatus.ioCount, took }
  111. }
  112. }
  113. const _checkFile = dbPath => {
  114. try {
  115. fs.accessSync(dbPath, fs.constants.F_OK)
  116. } catch (err) {
  117. throw new Error(`${dbPath} ${err ? 'does not exist' : 'exists'}`)
  118. }
  119. try {
  120. fs.accessSync(dbPath, fs.constants.R_OK)
  121. } catch (err) {
  122. throw new Error(`${dbPath} ${err ? 'is not readable' : 'is readable'}`)
  123. }
  124. }
  125. const isValidIp = ip => {
  126. return IP_REGEX.test(ip)
  127. }
  128. const newWithFileOnly = dbPath => {
  129. _checkFile(dbPath)
  130. return new Searcher(dbPath, null, null)
  131. }
  132. const newWithVectorIndex = (dbPath, vectorIndex) => {
  133. _checkFile(dbPath)
  134. if (!Buffer.isBuffer(vectorIndex)) {
  135. throw new Error('vectorIndex is invalid')
  136. }
  137. return new Searcher(dbPath, vectorIndex, null)
  138. }
  139. const newWithBuffer = buffer => {
  140. if (!Buffer.isBuffer(buffer)) {
  141. throw new Error('buffer is invalid')
  142. }
  143. return new Searcher(null, null, buffer)
  144. }
  145. const loadVectorIndexFromFile = dbPath => {
  146. const fd = fs.openSync(dbPath, 'r')
  147. const buffer = Buffer.alloc(VectorIndexLength)
  148. fs.readSync(fd, buffer, 0, VectorIndexLength, 256)
  149. fs.close(fd,function(){})
  150. return buffer
  151. }
  152. const loadContentFromFile = dbPath => {
  153. const stats = fs.statSync(dbPath)
  154. const buffer = Buffer.alloc(stats.size)
  155. const fd = fs.openSync(dbPath, 'r')
  156. fs.readSync(fd, buffer, 0, stats.size, 0)
  157. fs.close(fd,function(){})
  158. return buffer
  159. }
  160. module.exports = {
  161. isValidIp,
  162. loadVectorIndexFromFile,
  163. loadContentFromFile,
  164. newWithFileOnly,
  165. newWithVectorIndex,
  166. newWithBuffer
  167. }