Skip to content

Commit 5fef486

Browse files
authored
Bugs/fix_persistent_storage (#1329)
* expose persistentStorage on node status
1 parent a1e1316 commit 5fef486

10 files changed

Lines changed: 114 additions & 28 deletions

File tree

docs/env.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ Environmental variables are also tracked in `ENVIRONMENT_VARIABLES` within `src/
3434
- `AUTHORIZED_PUBLISHERS_LIST`: AccessList contract addresses (per chain). If present, Node will only index assets published by the accounts present on the given access lists. Example: `"{ \"8996\": [\"0x967da4048cD07aB37855c090aAF366e4ce1b9F48\",\"0x388C818CA8B9251b393131C08a736A67ccB19297\"] }"`
3535
- `VALIDATE_UNSIGNED_DDO`: If set to `false`, the node will not validate unsigned DDOs and will request a signed message with the publisher address, nonce and signature. Default is `true`. Example: `false`
3636
- `JWT_SECRET`: Secret used to sign JWT tokens. Default is `ocean-node-secret`. Example: `"my-secret-jwt-token"`
37+
- `PERSISTENT_STORAGE`: Persistent storage config. See [persistent storage](persistentStorage.md).
3738

3839
## Database
3940

src/@types/OceanNode.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { FeeStrategy } from './Fees'
55
import { Schema } from '../components/database'
66
import { KeyProviderType } from './KeyManager'
77
import type { PersistentStorageConfig } from './PersistentStorage.js'
8+
import type { AccessList } from './AccessList'
89

910
export interface OceanNodeDBConfig {
1011
url: string | null
@@ -194,6 +195,9 @@ export interface OceanNodeStatus {
194195
// detailed information
195196
c2dClusters?: any[]
196197
supportedSchemas?: Schema[]
198+
persistentStorage?: {
199+
accessLists?: AccessList[]
200+
}
197201
}
198202

199203
export interface FindDDOResponse {

src/components/core/handler/persistentStorage.ts

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -69,15 +69,24 @@ export class PersistentStorageCreateBucketHandler extends CommandHandler {
6969
const storage = requirePersistentStorage(this)
7070
const node = this.getOceanNode()
7171
const config = node.getConfig()
72-
const isAllowedCreate = await checkAddressOnAccessList(
73-
task.consumerAddress,
74-
config.persistentStorage?.accessLists,
75-
node
76-
)
77-
if (!isAllowedCreate) {
78-
return {
79-
stream: null,
80-
status: { httpStatus: 403, error: 'You are not allowed to create new buckets' }
72+
// if we have access lists,check them.
73+
if (
74+
config.persistentStorage?.accessLists &&
75+
config.persistentStorage?.accessLists.length > 0
76+
) {
77+
const isAllowedCreate = await checkAddressOnAccessList(
78+
task.consumerAddress,
79+
config.persistentStorage?.accessLists,
80+
node
81+
)
82+
if (!isAllowedCreate) {
83+
return {
84+
stream: null,
85+
status: {
86+
httpStatus: 403,
87+
error: 'You are not allowed to create new buckets'
88+
}
89+
}
8190
}
8291
}
8392

src/components/core/utils/statusHandler.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import {
77
StorageTypes,
88
OceanNodeConfig
99
} from '../../../@types/OceanNode.js'
10-
import { getConfiguration } from '../../../utils/index.js'
1110
import { CORE_LOGGER } from '../../../utils/logging/common.js'
1211
import { OceanNode } from '../../../OceanNode.js'
1312
import { typesenseSchemas } from '../../database/TypesenseSchemas.js'
@@ -112,7 +111,7 @@ export async function status(
112111
)
113112
return
114113
}
115-
const config = await getConfiguration()
114+
const config = oceanNode.getConfig()
116115

117116
// no previous status?
118117
if (!nodeStatus) {
@@ -173,5 +172,11 @@ export async function status(
173172
}
174173
nodeStatus.supportedSchemas = typesenseSchemas.ddoSchemas
175174
}
175+
176+
if (config.persistentStorage) {
177+
nodeStatus.persistentStorage = {}
178+
if (config.persistentStorage.accessLists)
179+
nodeStatus.persistentStorage.accessLists = config.persistentStorage.accessLists
180+
}
176181
return nodeStatus
177182
}

src/components/httpRoutes/persistentStorage.ts

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,6 @@ import {
1616

1717
export const persistentStorageRoutes = express.Router()
1818

19-
function readRawBody(req: any): Promise<Buffer> {
20-
return new Promise((resolve, reject) => {
21-
const chunks: Buffer[] = []
22-
req.on('data', (chunk: any) => chunks.push(Buffer.from(chunk)))
23-
req.on('end', () => resolve(Buffer.concat(chunks)))
24-
req.on('error', reject)
25-
})
26-
}
27-
2819
// Create bucket
2920
persistentStorageRoutes.post(
3021
`${SERVICES_API_BASE_PATH}/persistentStorage/buckets`,
@@ -144,7 +135,6 @@ persistentStorageRoutes.post(
144135
`${SERVICES_API_BASE_PATH}/persistentStorage/buckets/:bucketId/files/:fileName`,
145136
async (req, res) => {
146137
try {
147-
const raw = await readRawBody(req)
148138
const response = await new PersistentStorageUploadFileHandler(req.oceanNode).handle(
149139
{
150140
command: PROTOCOL_COMMANDS.PERSISTENT_STORAGE_UPLOAD_FILE,
@@ -153,7 +143,8 @@ persistentStorageRoutes.post(
153143
nonce: req.query.nonce as string,
154144
bucketId: req.params.bucketId,
155145
fileName: req.params.fileName,
156-
stream: Readable.from(raw),
146+
// Stream request body directly (supports chunked uploads, avoids buffering).
147+
stream: req,
157148
authorization: req.headers?.authorization,
158149
caller: req.caller
159150
} as any

src/components/persistentStorage/PersistentStorageLocalFS.ts

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
PersistentStorageFileInfo
1919
} from './PersistentStorageFactory.js'
2020
import { OceanNode } from '../../OceanNode.js'
21+
import { CORE_LOGGER } from '../../utils/logging/common.js'
2122

2223
export class PersistentStorageLocalFS extends PersistentStorageFactory {
2324
/* eslint-disable security/detect-non-literal-fs-filename -- localfs backend operates on filesystem paths */
@@ -29,7 +30,26 @@ export class PersistentStorageLocalFS extends PersistentStorageFactory {
2930
.options as PersistentStorageLocalFSOptions
3031

3132
this.baseFolder = options.folder
32-
fsp.mkdir(this.baseFolder, { recursive: true })
33+
34+
// Ensure base folder exists and is a directory (sync to avoid startup races).
35+
try {
36+
fs.mkdirSync(this.baseFolder, { recursive: true })
37+
const st = fs.statSync(this.baseFolder)
38+
if (!st.isDirectory()) {
39+
throw new Error(
40+
`Persistent storage folder is not a directory: ${this.baseFolder}`
41+
)
42+
}
43+
fs.mkdirSync(path.join(this.baseFolder, 'buckets'), { recursive: true })
44+
} catch (e: any) {
45+
if (e?.code === 'EACCES') {
46+
throw new Error(
47+
`Persistent storage folder is not accessible (EACCES): ${this.baseFolder}. ` +
48+
`Configure 'persistentStorage.options.folder' to a writable path inside the container and mount it as a volume.`
49+
)
50+
}
51+
throw e
52+
}
3353
}
3454

3555
private bucketPath(bucketId: string): string {
@@ -78,7 +98,9 @@ export class PersistentStorageLocalFS extends PersistentStorageFactory {
7898
): Promise<CreateBucketResult> {
7999
const bucketId = randomUUID()
80100
const createdAt = Math.floor(Date.now() / 1000)
81-
await fsp.mkdir(this.bucketPath(bucketId), { recursive: true })
101+
const path = this.bucketPath(bucketId)
102+
CORE_LOGGER.debug(`Creating ${path} folder for new bucket`)
103+
await fsp.mkdir(path)
82104
await super.dbUpsertBucket(
83105
bucketId,
84106
owner,

src/test/integration/persistentStorage.test.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,12 @@ import {
1414
PersistentStorageListFilesHandler,
1515
PersistentStorageUploadFileHandler
1616
} from '../../components/core/handler/persistentStorage.js'
17+
import { StatusHandler } from '../../components/core/handler/statusHandler.js'
1718
import { OceanNode } from '../../OceanNode.js'
1819
import type { AccessList } from '../../@types/AccessList.js'
1920
import { ENVIRONMENT_VARIABLES, PROTOCOL_COMMANDS } from '../../utils/constants.js'
2021
import { getConfiguration } from '../../utils/config.js'
21-
import { streamToObject } from '../../utils/util.js'
22+
import { streamToObject, streamToString } from '../../utils/util.js'
2223
import {
2324
DEFAULT_TEST_TIMEOUT,
2425
OverrideEnvConfig,
@@ -35,7 +36,7 @@ import { Blockchain } from '../../utils/blockchain.js'
3536
import { RPCS, SupportedNetwork } from '../../@types/blockchain.js'
3637
import { DEVELOPMENT_CHAIN_ID } from '../../utils/address.js'
3738
import { deployAndGetAccessListConfig } from '../utils/contracts.js'
38-
import { OceanNodeConfig } from '../../@types/OceanNode.js'
39+
import { OceanNodeConfig, OceanNodeStatus } from '../../@types/OceanNode.js'
3940
import { KeyManager } from '../../components/KeyManager/index.js'
4041

4142
describe('Persistent storage handlers (integration)', function () {
@@ -119,6 +120,19 @@ describe('Persistent storage handlers (integration)', function () {
119120
// await fsp.rm(psRoot, { recursive: true, force: true })
120121
})
121122

123+
it('should expose persistent storage access lists on node status', async () => {
124+
const statusCommand = {
125+
command: PROTOCOL_COMMANDS.STATUS,
126+
node: oceanNode.getKeyManager().getPeerId().toString()
127+
}
128+
const response = await new StatusHandler(oceanNode).handle(statusCommand)
129+
expect(response.status.httpStatus).to.equal(200)
130+
const body = await streamToString(response.stream as Readable)
131+
const nodeStatus = JSON.parse(body) as OceanNodeStatus
132+
expect(nodeStatus.persistentStorage).to.be.an('object')
133+
expect(nodeStatus.persistentStorage?.accessLists).to.be.an('array').with.lengthOf(1)
134+
})
135+
122136
it('create bucket → upload → list → delete (happy path)', async () => {
123137
const consumerAddress = await consumer.getAddress()
124138
let nonce = Date.now().toString()

src/utils/config/constants.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,8 @@ export const ENV_TO_CONFIG_MAPPING = {
6969
P2P_ENABLE_NETWORK_STATS: 'p2pConfig.enableNetworkStats',
7070
HTTP_CERT_PATH: 'httpCertPath',
7171
HTTP_KEY_PATH: 'httpKeyPath',
72-
ENABLE_BENCHMARK: 'enableBenchmark'
72+
ENABLE_BENCHMARK: 'enableBenchmark',
73+
PERSISTENT_STORAGE: 'persistentStorage'
7374
} as const
7475

7576
// Configuration defaults

src/utils/config/schemas.ts

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -389,7 +389,41 @@ export const OceanNodeConfigSchema = z
389389
DB_PASSWORD: z.string().optional(),
390390
DB_TYPE: z.string().optional(),
391391
dbConfig: OceanNodeDBConfigSchema.optional(),
392-
persistentStorage: PersistentStorageConfigSchema.optional(),
392+
// Accept either an object (config file) or a JSON string (env var `PERSISTENT_STORAGE`),
393+
// and validate the parsed value against the PersistentStorage schema.
394+
persistentStorage: z
395+
.preprocess((val) => {
396+
if (val === undefined || val === null) return val
397+
if (typeof val === 'string') {
398+
const tryParse = (s: string) => {
399+
try {
400+
return JSON.parse(s)
401+
} catch {
402+
return undefined
403+
}
404+
}
405+
406+
// 1) Normal JSON string
407+
const parsed = tryParse(val)
408+
if (parsed !== undefined) {
409+
// 2) Handle double-encoded JSON (e.g. "\"{...}\"")
410+
if (typeof parsed === 'string') {
411+
const parsedTwice = tryParse(parsed)
412+
if (parsedTwice !== undefined) return parsedTwice
413+
}
414+
return parsed
415+
}
416+
417+
// 3) Common docker-compose/shell mistake: single quotes inside JSON
418+
const normalized = val.replace(/'/g, '"')
419+
const parsedNormalized = tryParse(normalized)
420+
if (parsedNormalized !== undefined) return parsedNormalized
421+
422+
return val
423+
}
424+
return val
425+
}, PersistentStorageConfigSchema)
426+
.optional(),
393427

394428
FEE_AMOUNT: z.string().optional(),
395429
FEE_TOKENS: z.string().optional(),

src/utils/constants.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -526,6 +526,11 @@ export const ENVIRONMENT_VARIABLES: Record<any, EnvVariable> = {
526526
name: 'HTTP_KEY_PATH',
527527
value: process.env.HTTP_KEY_PATH,
528528
required: false
529+
},
530+
PERSISTENT_STORAGE: {
531+
name: 'PERSISTENT_STORAGE',
532+
value: process.env.PERSISTENT_STORAGE,
533+
required: false
529534
}
530535
}
531536
export const CONNECTION_HISTORY_DELETE_THRESHOLD = 300

0 commit comments

Comments
 (0)