@openneuro/server 4.4.9 → 4.5.1

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openneuro/server",
3
- "version": "4.4.9",
3
+ "version": "4.5.1",
4
4
  "description": "Core service for the OpenNeuro platform.",
5
5
  "license": "MIT",
6
6
  "main": "src/server.js",
@@ -47,7 +47,7 @@
47
47
  "jsonwebtoken": "^8.3.0",
48
48
  "mime-types": "^2.1.19",
49
49
  "moment": "^2.14.1",
50
- "mongoose": "6.2.0",
50
+ "mongoose": "^6.2.3",
51
51
  "morgan": "^1.6.1",
52
52
  "node-fetch": "^2.6.0",
53
53
  "node-mailjet": "^3.3.5",
@@ -104,5 +104,5 @@
104
104
  "publishConfig": {
105
105
  "access": "public"
106
106
  },
107
- "gitHead": "af313c7d80f6aade5786075853d00023c52db60e"
107
+ "gitHead": "92a2879d0f00400c240bfe3319f26c38db8ef866"
108
108
  }
@@ -3,6 +3,7 @@ import {
3
3
  decodeFilePath,
4
4
  fileUrl,
5
5
  filterFiles,
6
+ computeTotalSize,
6
7
  } from '../files.js'
7
8
 
8
9
  jest.mock('../../config.js')
@@ -104,4 +105,16 @@ describe('datalad files', () => {
104
105
  expect(filterFiles('sub-01/func')(mockFiles)).toEqual([mockSub01[1]])
105
106
  })
106
107
  })
108
+ describe('computeTotalSize()', () => {
109
+ const mockFileSizes = [
110
+ { filename: 'README', size: 234 },
111
+ { filename: 'dataset_description.json', size: 432 },
112
+ { filename: 'sub-01/anat/sub-01_T1w.nii.gz', size: 10858 },
113
+ {
114
+ filename: 'sub-01/func/sub-01_task-onebacktask_run-01_bold.nii.gz',
115
+ size: 1945682,
116
+ },
117
+ ]
118
+ expect(computeTotalSize(mockFileSizes)).toBe(1957206)
119
+ })
107
120
  })
@@ -127,6 +127,7 @@ export const description = obj => {
127
127
  return cache
128
128
  .get(() => {
129
129
  return getFiles(datasetId, revision)
130
+ .then(response => response.files)
130
131
  .then(getDescriptionObject(datasetId))
131
132
  .then(uncachedDescription => ({ id: revision, ...uncachedDescription }))
132
133
  })
@@ -51,6 +51,12 @@ export const fileUrl = (datasetId, path, filename) => {
51
51
  export const filesUrl = datasetId =>
52
52
  `http://${getDatasetWorker(datasetId)}/datasets/${datasetId}/files`
53
53
 
54
+ /**
55
+ * Sum all file sizes for total dataset size
56
+ */
57
+ export const computeTotalSize = files =>
58
+ files.reduce((size, f) => size + f.size, 0)
59
+
54
60
  /**
55
61
  * Get files for a specific revision
56
62
  * Similar to getDraftFiles but different cache key and fixed revisions
@@ -75,7 +81,11 @@ export const getFiles = (datasetId, hexsha) => {
75
81
  const {
76
82
  body: { files },
77
83
  } = response
78
- return files.map(addFileUrl(datasetId, hexsha))
84
+ const size = computeTotalSize(files)
85
+ return {
86
+ files: files.map(addFileUrl(datasetId, hexsha)),
87
+ size,
88
+ }
79
89
  }
80
90
  }),
81
91
  )
@@ -154,7 +154,9 @@ export const createSnapshot = async (
154
154
  snapshotChanges,
155
155
  )
156
156
  snapshot.created = new Date()
157
- snapshot.files = await getFiles(datasetId, tag)
157
+ const { files, size } = await getFiles(datasetId, tag)
158
+ snapshot.files = files
159
+ snapshot.size = size
158
160
 
159
161
  await Promise.all([
160
162
  // Update the draft status in datasets collection in case any changes were made (DOI, License)
@@ -1,3 +1,4 @@
1
+ import fetch from 'node-fetch'
1
2
  import {
2
3
  DatasetOrSnapshot,
3
4
  getDatasetFromSnapshotId,
@@ -16,9 +16,7 @@ import { history } from './history.js'
16
16
  import * as dataladAnalytics from '../../datalad/analytics.js'
17
17
  import DatasetModel from '../../models/dataset'
18
18
  import Deletion from '../../models/deletion'
19
- import fetch from 'node-fetch'
20
19
  import { reviewers } from './reviewer'
21
- import { UpdatedFile } from '../utils/file.js'
22
20
  import { getDatasetWorker } from '../../libs/datalad-service.js'
23
21
  import { getDraftHead } from '../../datalad/dataset.js'
24
22
  import { getFileName } from '../../datalad/files.js'
@@ -152,9 +150,7 @@ export const deleteFiles = async (
152
150
  ) => {
153
151
  try {
154
152
  await checkDatasetWrite(datasetId, user, userInfo)
155
- const deletedFiles = await datalad
156
- .deleteFiles(datasetId, files, userInfo)
157
- .then(filenames => filenames.map(filename => new UpdatedFile(filename)))
153
+ const deletedFiles = await datalad.deleteFiles(datasetId, files, userInfo)
158
154
  pubsub.publish('filesUpdated', {
159
155
  datasetId,
160
156
  filesUpdated: {
@@ -10,11 +10,17 @@ import { filterRemovedAnnexObjects } from '../utils/file.js'
10
10
  // A draft must have a dataset parent
11
11
  const draftFiles = async (dataset, args, { userInfo }) => {
12
12
  const hexsha = await getDraftRevision(dataset.id)
13
- const files = await getFiles(dataset.id, hexsha)
13
+ const { files } = await getFiles(dataset.id, hexsha)
14
14
  const prefixFiltered = filterFiles('prefix' in args && args.prefix)(files)
15
15
  return filterRemovedAnnexObjects(dataset.id, userInfo)(prefixFiltered)
16
16
  }
17
17
 
18
+ const draftSize = async (dataset, args, { userInfo }) => {
19
+ const hexsha = await getDraftRevision(dataset.id)
20
+ const { size } = await getFiles(dataset.id, hexsha)
21
+ return size
22
+ }
23
+
18
24
  /**
19
25
  * Deprecated mutation to move the draft HEAD reference forward or backward
20
26
  *
@@ -39,6 +45,7 @@ export const revalidate = async (obj, { datasetId }, { user, userInfo }) => {
39
45
  const draft = {
40
46
  id: obj => obj.id,
41
47
  files: draftFiles,
48
+ size: draftSize,
42
49
  summary,
43
50
  issues,
44
51
  modified: obj => obj.modified,
@@ -44,7 +44,9 @@ const publishPermissions = async datasetId => {
44
44
  export const updatePermissions = async (obj, args, { user, userInfo }) => {
45
45
  await checkDatasetAdmin(args.datasetId, user, userInfo)
46
46
  // get all users the the email specified by permissions arg
47
- const users = await User.find({ email: args.userEmail }).exec()
47
+ const users = await User.find({ email: args.userEmail })
48
+ .collation({ locale: 'en', strength: 2 })
49
+ .exec()
48
50
 
49
51
  if (!users.length) {
50
52
  throw new Error('A user with that email address does not exist')
@@ -29,8 +29,11 @@ export const snapshot = (obj, { datasetId, tag }, context) => {
29
29
  summary: () => summary({ id: datasetId, revision: snapshot.hexsha }),
30
30
  files: ({ prefix }) =>
31
31
  getFiles(datasetId, snapshot.hexsha)
32
+ .then(response => response.files)
32
33
  .then(filterFiles(prefix))
33
34
  .then(filterRemovedAnnexObjects(datasetId, context.userInfo)),
35
+ size: () =>
36
+ getFiles(datasetId, snapshot.hexsha).then(response => response.size),
34
37
  deprecated: () => deprecated({ datasetId, tag }),
35
38
  related: () => related(datasetId),
36
39
  onBrainlife: () => onBrainlife(snapshot),
@@ -430,6 +430,8 @@ export const typeDefs = `
430
430
  uploads: [UploadMetadata]
431
431
  # Git commit hash
432
432
  head: String
433
+ # Total size in bytes of this draft
434
+ size: Int
433
435
  }
434
436
 
435
437
  # Tagged snapshot of a draft
@@ -461,6 +463,8 @@ export const typeDefs = `
461
463
  related: [RelatedObject]
462
464
  # Is the snapshot available for analysis on Brainlife?
463
465
  onBrainlife: Boolean @cacheControl(maxAge: 10080, scope: PUBLIC)
466
+ # Total size in bytes of this snapshot
467
+ size: Int
464
468
  }
465
469
 
466
470
  # RelatedObject nature of relationship
@@ -1,34 +1,5 @@
1
1
  import BadAnnexObject from '../../models/badAnnexObject'
2
2
 
3
- /**
4
- * Generates unique id for untracked files.
5
- * @param {string} filepath - filepath ('/' delimiters)
6
- * @param {number|string} [size] - of file
7
- */
8
- export const generateFileId = (filepath, size) => `${filepath}:${size}`
9
-
10
- /**
11
- * Creates a file object with an ApolloGQL cache-safe id.
12
- * @class
13
- * @param {string} filepath ':' delimited
14
- * @param {string|number} [size]
15
- */
16
- export function UpdatedFile(filepath, size) {
17
- /**
18
- * unique id
19
- * @id UpdatedFile#id
20
- * @type {string}
21
- */
22
- this.id = generateFileId(filepath, size)
23
- /**
24
- * filename with '/' delimiters
25
- * @filename UpdatedFile#filename
26
- * @type {string}
27
- */
28
- this.filename = filepath
29
- if (size) this.size = size
30
- }
31
-
32
3
  export const filterRemovedAnnexObjects =
33
4
  (datasetId, userInfo) => async files => {
34
5
  const removedAnnexObjectKeys = (
@@ -29,6 +29,16 @@ const userSchema = new Schema({
29
29
  })
30
30
 
31
31
  userSchema.index({ id: 1, provider: 1 }, { unique: true })
32
+ // Allow case insensitive email queries
33
+ userSchema.index(
34
+ { email: 1 },
35
+ {
36
+ collation: {
37
+ locale: 'en',
38
+ strength: 2,
39
+ },
40
+ },
41
+ )
32
42
 
33
43
  const User = model<UserDocument>('User', userSchema)
34
44