Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,10 @@ export class Context {
await writer.write(chunk)
} catch (err) {
await fsPromises.unlink(tempPath).catch(noop)
throw err
throw new errors.MAP_WRITE_ERROR({
message: err instanceof Error ? err.message : undefined,
cause: err,
})
}
},
close: async () => {
Expand All @@ -143,7 +146,10 @@ export class Context {
await writer.close()
} catch (err) {
await fsPromises.unlink(tempPath).catch(noop)
throw err
throw new errors.MAP_WRITE_ERROR({
message: err instanceof Error ? err.message : undefined,
cause: err,
})
}

// Validate the uploaded map file BEFORE replacing the existing one
Expand Down
5 changes: 5 additions & 0 deletions src/lib/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,11 @@ const errorsList = [
message: 'Invalid map file',
status: 400,
},
{
code: 'MAP_WRITE_ERROR',
message: 'Failed to write map data',
status: 500,
},

// Generic errors
{
Expand Down
63 changes: 58 additions & 5 deletions test/map-shares.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
fetch as secretStreamFetch,
Agent as SecretStreamAgent,
} from 'secret-stream-http'
import { describe, it, expect } from 'vitest'
import { describe, it, expect, vi } from 'vitest'

import type { MapShareState } from '../src/types.js'
import {
Expand Down Expand Up @@ -150,7 +150,9 @@ describe('Map Shares and Downloads', () => {

// Create a third device (another receiver)
const receiver2KeyPair = SecretStreamAgent.keyPair(Buffer.alloc(32, 2))
const receiver2DeviceId = Buffer.from(receiver2KeyPair.publicKey).toString('hex')
const receiver2DeviceId = Buffer.from(
receiver2KeyPair.publicKey,
).toString('hex')

// Create share for first receiver
const share1 = await createShare().json()
Expand Down Expand Up @@ -1104,7 +1106,9 @@ describe('Map Shares and Downloads', () => {

// Create a third device with different keys
const wrongKeyPair = SecretStreamAgent.keyPair()
const wrongDeviceId = Buffer.from(wrongKeyPair.publicKey).toString('hex')
const wrongDeviceId = Buffer.from(wrongKeyPair.publicKey).toString(
'hex',
)

// Create a share for a different device
const { shareId: shareId2 } = await sender
Expand Down Expand Up @@ -1134,7 +1138,9 @@ describe('Map Shares and Downloads', () => {

// Create a third device with different keys
const wrongKeyPair = SecretStreamAgent.keyPair()
const wrongDeviceId = Buffer.from(wrongKeyPair.publicKey).toString('hex')
const wrongDeviceId = Buffer.from(wrongKeyPair.publicKey).toString(
'hex',
)

// Create a share for wrongDeviceId
const { shareId } = await sender
Expand Down Expand Up @@ -1187,7 +1193,9 @@ describe('Map Shares and Downloads', () => {

// Create a third device
const wrongKeyPair = SecretStreamAgent.keyPair()
const wrongDeviceId = Buffer.from(wrongKeyPair.publicKey).toString('hex')
const wrongDeviceId = Buffer.from(wrongKeyPair.publicKey).toString(
'hex',
)

// Create a share for wrongDeviceId
const { shareId } = await sender
Expand Down Expand Up @@ -1638,6 +1646,51 @@ describe('Map Shares and Downloads', () => {
expect(tempFiles).toHaveLength(0)
})

it('should report MAP_WRITE_ERROR when write fails during download', async (t) => {
const { createShare, createDownload, receiver } = await startServers(t)
const receiverDir = path.dirname(receiver.customMapPath)
const receiverBasename = path.basename(receiver.customMapPath)

// Mock fs.createWriteStream on the receiver to simulate disk-full
const originalCreateWriteStream = fs.createWriteStream
const spy = vi
.spyOn(fs, 'createWriteStream')
.mockImplementation((...args: any[]) => {
const stream = originalCreateWriteStream.apply(fs, args as any)
stream._write = (
_chunk: any,
_encoding: BufferEncoding,
callback: (error?: Error | null) => void,
) => {
callback(new Error('ENOSPC: no space left on device'))
}
return stream
})
t.onTestFinished(() => spy.mockRestore())

const share = await createShare().json()
const download = await createDownload(share).json<any>()

// Wait for download to fail
await eventsUntil(receiver, download.downloadId, 'error')

// Check download state has MAP_WRITE_ERROR
const downloadStatus = await receiver
.get(`downloads/${download.downloadId}`)
.json<any>()
expect(downloadStatus.status).toBe('error')
expect(downloadStatus).toHaveProperty('error.code', 'MAP_WRITE_ERROR')

await delay(100)

// Verify temp files are cleaned up
const files = fs.readdirSync(receiverDir)
const tempFiles = files.filter(
(f) => f.startsWith(receiverBasename) && f.includes('.download-'),
)
expect(tempFiles).toHaveLength(0)
})

it('should try next URL when first mapShareUrl fails during download', async (t) => {
const { createShare, sender, receiver } = await startServers(t)

Expand Down
4 changes: 4 additions & 0 deletions test/maps.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -603,6 +603,8 @@ describe('Map Upload', () => {
})

expect(response.status).toBe(500)
const error = await response.json()
expect(error).toHaveProperty('code', 'MAP_WRITE_ERROR')

// Allow time for async cleanup to complete
await delay(100)
Expand Down Expand Up @@ -646,6 +648,8 @@ describe('Map Upload', () => {
})

expect(response.status).toBe(500)
const error = await response.json()
expect(error).toHaveProperty('code', 'MAP_WRITE_ERROR')

// Allow time for async cleanup to complete
await delay(100)
Expand Down