braid-blob 0.0.41 → 0.0.42

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/index.js +20 -4
  2. package/package.json +1 -1
  3. package/test/tests.js +206 -0
package/index.js CHANGED
@@ -11,6 +11,8 @@ function create_braid_blob() {
11
11
  db: null // object with read/write/delete methods
12
12
  }
13
13
 
14
+ var temp_folder = null // will be set in init
15
+
14
16
  braid_blob.init = async () => {
15
17
  // We only want to initialize once
16
18
  var init_p = real_init()
@@ -21,6 +23,15 @@ function create_braid_blob() {
21
23
  // Ensure our meta folder exists
22
24
  await require('fs').promises.mkdir(braid_blob.meta_folder, { recursive: true })
23
25
 
26
+ // Create a temp folder inside the meta folder for writing temp files,
27
+ // for atomic writing.
28
+ // The temp folder is called "temp",
29
+ // And this is guaranteed not to conflict with any other files,
30
+ // because other files are the result of encode_filename,
31
+ // which always ends with a ".XX" (for handling insensitive filesystems)
32
+ temp_folder = `${braid_blob.meta_folder}/temp`
33
+ await require('fs').promises.mkdir(temp_folder, { recursive: true })
34
+
24
35
  // Set up db - either use provided object or create file-based storage
25
36
  if (typeof braid_blob.db_folder === 'string') {
26
37
  await require('fs').promises.mkdir(braid_blob.db_folder, { recursive: true })
@@ -36,7 +47,7 @@ function create_braid_blob() {
36
47
  },
37
48
  write: async (key, data) => {
38
49
  var file_path = `${braid_blob.db_folder}/${encode_filename(key)}`
39
- await require('fs').promises.writeFile(file_path, data)
50
+ await atomic_write(file_path, data, temp_folder)
40
51
  },
41
52
  delete: async (key) => {
42
53
  var file_path = `${braid_blob.db_folder}/${encode_filename(key)}`
@@ -74,9 +85,8 @@ function create_braid_blob() {
74
85
  }
75
86
 
76
87
  async function save_meta(key) {
77
- await require('fs').promises.writeFile(
78
- `${braid_blob.meta_folder}/${encode_filename(key)}`,
79
- JSON.stringify(braid_blob.meta_cache[key]))
88
+ await atomic_write(`${braid_blob.meta_folder}/${encode_filename(key)}`,
89
+ JSON.stringify(braid_blob.meta_cache[key]), temp_folder)
80
90
  }
81
91
 
82
92
  async function delete_meta(key) {
@@ -733,6 +743,12 @@ function create_braid_blob() {
733
743
  return normalized
734
744
  }
735
745
 
746
+ async function atomic_write(final_destination, data, temp_folder) {
747
+ var temp = `${temp_folder}/${Math.random().toString(36).slice(2)}`
748
+ await require('fs').promises.writeFile(temp, data)
749
+ await require('fs').promises.rename(temp, final_destination)
750
+ }
751
+
736
752
  braid_blob.create_braid_blob = create_braid_blob
737
753
 
738
754
  return braid_blob
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "braid-blob",
3
- "version": "0.0.41",
3
+ "version": "0.0.42",
4
4
  "description": "Library for collaborative blobs over http using braid.",
5
5
  "author": "Braid Working Group",
6
6
  "repository": "braid-org/braid-blob",
package/test/tests.js CHANGED
@@ -1799,6 +1799,212 @@ runTest(
1799
1799
  'true'
1800
1800
  )
1801
1801
 
1802
+ runTest(
1803
+ "test atomic write creates temp folder on init",
1804
+ async () => {
1805
+ var r1 = await braid_fetch(`/eval`, {
1806
+ method: 'POST',
1807
+ body: `void (async () => {
1808
+ var fs = require('fs').promises
1809
+ var test_id = 'test-atomic-init-' + Math.random().toString(36).slice(2)
1810
+ var db_folder = __dirname + '/' + test_id + '-db'
1811
+ var meta_folder = __dirname + '/' + test_id + '-meta'
1812
+
1813
+ try {
1814
+ var bb = braid_blob.create_braid_blob()
1815
+ bb.db_folder = db_folder
1816
+ bb.meta_folder = meta_folder
1817
+
1818
+ // Initialize
1819
+ await bb.init()
1820
+
1821
+ // Check that temp folder exists inside meta folder
1822
+ var temp_folder = meta_folder + '/temp'
1823
+ var stat = await fs.stat(temp_folder)
1824
+ var is_dir = stat.isDirectory()
1825
+
1826
+ res.end(is_dir ? 'true' : 'not a directory')
1827
+ } catch (e) {
1828
+ res.end('error: ' + e.message)
1829
+ } finally {
1830
+ await fs.rm(db_folder, { recursive: true, force: true })
1831
+ await fs.rm(meta_folder, { recursive: true, force: true })
1832
+ }
1833
+ })()`
1834
+ })
1835
+ return await r1.text()
1836
+ },
1837
+ 'true'
1838
+ )
1839
+
1840
+ runTest(
1841
+ "test atomic write leaves no temp files after successful write",
1842
+ async () => {
1843
+ var r1 = await braid_fetch(`/eval`, {
1844
+ method: 'POST',
1845
+ body: `void (async () => {
1846
+ var fs = require('fs').promises
1847
+ var test_id = 'test-atomic-cleanup-' + Math.random().toString(36).slice(2)
1848
+ var db_folder = __dirname + '/' + test_id + '-db'
1849
+ var meta_folder = __dirname + '/' + test_id + '-meta'
1850
+
1851
+ try {
1852
+ var bb = braid_blob.create_braid_blob()
1853
+ bb.db_folder = db_folder
1854
+ bb.meta_folder = meta_folder
1855
+
1856
+ // Do a write
1857
+ await bb.put('/test-file', Buffer.from('hello'), { version: ['1'] })
1858
+
1859
+ // Check that temp folder is empty (no leftover temp files)
1860
+ var temp_folder = meta_folder + '/temp'
1861
+ var files = await fs.readdir(temp_folder)
1862
+
1863
+ res.end(files.length === 0 ? 'true' : 'leftover files: ' + files.join(', '))
1864
+ } catch (e) {
1865
+ res.end('error: ' + e.message)
1866
+ } finally {
1867
+ await fs.rm(db_folder, { recursive: true, force: true })
1868
+ await fs.rm(meta_folder, { recursive: true, force: true })
1869
+ }
1870
+ })()`
1871
+ })
1872
+ return await r1.text()
1873
+ },
1874
+ 'true'
1875
+ )
1876
+
1877
+ runTest(
1878
+ "test atomic write data file integrity",
1879
+ async () => {
1880
+ var r1 = await braid_fetch(`/eval`, {
1881
+ method: 'POST',
1882
+ body: `void (async () => {
1883
+ var fs = require('fs').promises
1884
+ var test_id = 'test-atomic-integrity-' + Math.random().toString(36).slice(2)
1885
+ var db_folder = __dirname + '/' + test_id + '-db'
1886
+ var meta_folder = __dirname + '/' + test_id + '-meta'
1887
+
1888
+ try {
1889
+ var bb = braid_blob.create_braid_blob()
1890
+ bb.db_folder = db_folder
1891
+ bb.meta_folder = meta_folder
1892
+
1893
+ // Write initial content
1894
+ await bb.put('/test-file', Buffer.from('initial content'), { version: ['1'] })
1895
+
1896
+ // Verify we can read it back correctly
1897
+ var result = await bb.get('/test-file')
1898
+ var content = result.body.toString()
1899
+
1900
+ res.end(content === 'initial content' ? 'true' : 'wrong content: ' + content)
1901
+ } catch (e) {
1902
+ res.end('error: ' + e.message)
1903
+ } finally {
1904
+ await fs.rm(db_folder, { recursive: true, force: true })
1905
+ await fs.rm(meta_folder, { recursive: true, force: true })
1906
+ }
1907
+ })()`
1908
+ })
1909
+ return await r1.text()
1910
+ },
1911
+ 'true'
1912
+ )
1913
+
1914
+ runTest(
1915
+ "test atomic write - multiple rapid writes preserve last value",
1916
+ async () => {
1917
+ var r1 = await braid_fetch(`/eval`, {
1918
+ method: 'POST',
1919
+ body: `void (async () => {
1920
+ var fs = require('fs').promises
1921
+ var test_id = 'test-atomic-rapid-' + Math.random().toString(36).slice(2)
1922
+ var db_folder = __dirname + '/' + test_id + '-db'
1923
+ var meta_folder = __dirname + '/' + test_id + '-meta'
1924
+
1925
+ try {
1926
+ var bb = braid_blob.create_braid_blob()
1927
+ bb.db_folder = db_folder
1928
+ bb.meta_folder = meta_folder
1929
+
1930
+ // Do multiple rapid writes
1931
+ await bb.put('/test-file', Buffer.from('write1'), { version: ['1'] })
1932
+ await bb.put('/test-file', Buffer.from('write2'), { version: ['2'] })
1933
+ await bb.put('/test-file', Buffer.from('write3'), { version: ['3'] })
1934
+
1935
+ // Verify last write won
1936
+ var result = await bb.get('/test-file')
1937
+ var content = result.body.toString()
1938
+ var version = result.version[0]
1939
+
1940
+ // Also verify temp folder is clean
1941
+ var temp_folder = meta_folder + '/temp'
1942
+ var files = await fs.readdir(temp_folder)
1943
+
1944
+ res.end(content === 'write3' && version === '3' && files.length === 0 ? 'true' :
1945
+ 'content=' + content + ', version=' + version + ', temp_files=' + files.length)
1946
+ } catch (e) {
1947
+ res.end('error: ' + e.message)
1948
+ } finally {
1949
+ await fs.rm(db_folder, { recursive: true, force: true })
1950
+ await fs.rm(meta_folder, { recursive: true, force: true })
1951
+ }
1952
+ })()`
1953
+ })
1954
+ return await r1.text()
1955
+ },
1956
+ 'true'
1957
+ )
1958
+
1959
+ runTest(
1960
+ "test atomic write - meta file is also written atomically",
1961
+ async () => {
1962
+ var r1 = await braid_fetch(`/eval`, {
1963
+ method: 'POST',
1964
+ body: `void (async () => {
1965
+ var fs = require('fs').promises
1966
+ var test_id = 'test-atomic-meta-' + Math.random().toString(36).slice(2)
1967
+ var db_folder = __dirname + '/' + test_id + '-db'
1968
+ var meta_folder = __dirname + '/' + test_id + '-meta'
1969
+
1970
+ try {
1971
+ var bb = braid_blob.create_braid_blob()
1972
+ bb.db_folder = db_folder
1973
+ bb.meta_folder = meta_folder
1974
+
1975
+ // Write with content_type to test meta file
1976
+ await bb.put('/test-file', Buffer.from('content'), {
1977
+ version: ['test-version'],
1978
+ content_type: 'text/plain'
1979
+ })
1980
+
1981
+ // Create new instance to read from disk (not cache)
1982
+ var bb2 = braid_blob.create_braid_blob()
1983
+ bb2.db_folder = db_folder
1984
+ bb2.meta_folder = meta_folder
1985
+
1986
+ var result = await bb2.get('/test-file')
1987
+
1988
+ // Verify both version and content_type are correctly persisted
1989
+ var version_ok = result.version[0] === 'test-version'
1990
+ var ct_ok = result.content_type === 'text/plain'
1991
+
1992
+ res.end(version_ok && ct_ok ? 'true' :
1993
+ 'version_ok=' + version_ok + ', ct_ok=' + ct_ok +
1994
+ ', version=' + result.version[0] + ', ct=' + result.content_type)
1995
+ } catch (e) {
1996
+ res.end('error: ' + e.message)
1997
+ } finally {
1998
+ await fs.rm(db_folder, { recursive: true, force: true })
1999
+ await fs.rm(meta_folder, { recursive: true, force: true })
2000
+ }
2001
+ })()`
2002
+ })
2003
+ return await r1.text()
2004
+ },
2005
+ 'true'
2006
+ )
2007
+
1802
2008
  }
1803
2009
 
1804
2010
  // Export for Node.js (CommonJS)