Passed
Pull Request — filestream (#169)
by
unknown
01:50
created

file-write.ts ➔ removeId3TagIfFound   A

Complexity

Conditions 2

Size

Total Lines 14
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 12
dl 0
loc 14
rs 9.8
c 0
b 0
f 0
cc 2
1
import {
2
    fsWritePromise,
3
    fillBufferAsync,
4
    fillBufferSync,
5
    processFileSync,
6
    processFileAsync,
7
    fsExistsPromise,
8
    fsWriteFilePromise,
9
    fsRenamePromise,
10
    unlinkIfExistSync,
11
    unlinkIfExist
12
} from "./util-file"
13
import * as fs from 'fs'
14
import { Header, findId3TagPosition, getId3TagSize } from "./id3-tag"
15
import { WriteCallback, WriteOptions } from "./types/write"
16
import { hrtime } from "process"
17
18
const MinBufferSize = Header.size
19
const DefaultFileBufferSize = 20 * 1024 * 1024
20
21
export function writeId3TagToFileSync(
22
    filepath: string,
23
    id3Tag: Buffer,
24
    options: WriteOptions
25
): void {
26
    if (!fs.existsSync(filepath)) {
27
        fs.writeFileSync(filepath, id3Tag)
28
        return
29
    }
30
    const tempFilepath = makeTempFilepath(filepath)
31
    processFileSync(filepath, 'r', (readFileDescriptor) => {
32
        try {
33
            processFileSync(tempFilepath, 'w', (writeFileDescriptor) => {
34
                fs.writeSync(writeFileDescriptor, id3Tag)
35
                copyFileWithoutId3TagSync(
36
                    readFileDescriptor,
37
                    writeFileDescriptor,
38
                    getFileBufferSize(options)
39
                )
40
            })
41
        } catch(error) {
42
            unlinkIfExistSync(tempFilepath)
43
            throw error
44
        }
45
    })
46
    fs.renameSync(tempFilepath, filepath)
47
}
48
49
export function writeId3TagToFile(
50
    filepath: string,
51
    id3Tag: Buffer,
52
    options: WriteOptions,
53
    callback: WriteCallback
54
): void {
55
    writeId3TagToFileAsync(filepath, id3Tag, options)
56
    .then(
57
        () => callback(null),
58
        (error) => callback(error)
59
    )
60
}
61
62
export async function writeId3TagToFileAsync(
63
    filepath: string,
64
    id3Tag: Buffer,
65
    options: WriteOptions
66
): Promise<void> {
67
    if (!await fsExistsPromise(filepath)) {
68
        await fsWriteFilePromise(filepath, id3Tag)
69
        return
70
    }
71
    const tempFilepath = makeTempFilepath(filepath)
72
    await processFileAsync(filepath, 'r', async (readFileDescriptor) => {
73
        try {
74
            await processFileAsync(tempFilepath, 'w',
75
                async (writeFileDescriptor) => {
76
                    await fsWritePromise(writeFileDescriptor, id3Tag)
77
                    await copyFileWithoutId3TagAsync(
78
                        readFileDescriptor,
79
                        writeFileDescriptor,
80
                        getFileBufferSize(options)
81
                    )
82
                }
83
            )
84
        } catch(error) {
85
            await unlinkIfExist(tempFilepath)
86
            throw error
87
        }
88
89
    })
90
    await fsRenamePromise(tempFilepath, filepath)
91
}
92
93
function getFileBufferSize(options: WriteOptions) {
94
    return Math.max(
95
        options.fileBufferSize ?? DefaultFileBufferSize,
96
        MinBufferSize
97
    )
98
}
99
function makeTempFilepath(filepath: string) {
100
    // A high-resolution time is required to avoid potential conflicts
101
    // when running multiple tests in parallel for example.
102
    // Date.now() resolution is too low.
103
    return `${filepath}.tmp-${hrtime.bigint()}`
104
}
105
106
function copyFileWithoutId3TagSync(
107
    readFileDescriptor: number,
108
    writeFileDescriptor: number,
109
    fileBufferSize: number
110
) {
111
    const buffer = Buffer.alloc(fileBufferSize)
112
    let readData
113
    while((readData = fillBufferSync(readFileDescriptor, buffer)).length) {
114
        const { data, bytesToSkip } = removeId3TagIfFound(readData)
115
        if (bytesToSkip) {
116
            fillBufferSync(readFileDescriptor, Buffer.alloc(bytesToSkip))
117
        }
118
        fs.writeSync(writeFileDescriptor, data, 0, data.length, null)
119
    }
120
}
121
122
async function copyFileWithoutId3TagAsync(
123
    readFileDescriptor: number,
124
    writeFileDescriptor: number,
125
    fileBufferSize: number
126
) {
127
    const buffer = Buffer.alloc(fileBufferSize)
128
    let readData
129
    while((readData = await fillBufferAsync(readFileDescriptor, buffer)).length) {
130
        const { data, bytesToSkip } = removeId3TagIfFound(readData)
131
        if (bytesToSkip) {
132
            await fillBufferAsync(readFileDescriptor, Buffer.alloc(bytesToSkip))
133
        }
134
        await fsWritePromise(writeFileDescriptor, data, 0, data.length, null)
135
    }
136
}
137
138
function removeId3TagIfFound(data: Buffer) {
139
    const id3TagPosition = findId3TagPosition(data)
140
    if (id3TagPosition === -1) {
141
        return { data }
142
    }
143
    const dataFromId3Start = data.subarray(id3TagPosition)
144
    const id3TagSize = getId3TagSize(dataFromId3Start)
145
    return {
146
        data: Buffer.concat([
147
            data.subarray(0, id3TagPosition),
148
            dataFromId3Start.subarray(Math.min(id3TagSize, dataFromId3Start.length))
149
        ]),
150
        bytesToSkip: Math.max(0, id3TagSize - dataFromId3Start.length)
151
    }
152
}
153