-
Notifications
You must be signed in to change notification settings - Fork 70
Expand file tree
/
Copy pathimage.js
More file actions
104 lines (89 loc) · 2.75 KB
/
image.js
File metadata and controls
104 lines (89 loc) · 2.75 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
import { useEffect, useRef } from 'react'
import { XzReadableStream } from 'xz-decompress'
import { fetchStream } from './stream'
/**
* Progress callback
*
* @callback progressCallback
* @param {number} progress
* @returns {void}
*/
const MIN_QUOTA_GB = 5.25
export class ImageManager {
/** @type {FileSystemDirectoryHandle} */
root
async init() {
if (!this.root) {
this.root = await navigator.storage.getDirectory()
// Clean up any leftover files from previous sessions
try {
await this.root.remove({ recursive: true })
} catch (e) {
// Ignore errors - directory might not exist or be empty
console.debug('[ImageManager] Could not remove old directory:', e)
}
// Re-get the directory after removal
this.root = await navigator.storage.getDirectory()
console.info('[ImageManager] Initialized')
}
const estimate = await navigator.storage.estimate()
const quotaGB = (estimate.quota || 0) / (1024 ** 3)
if (quotaGB < MIN_QUOTA_GB) {
throw new Error(`Not enough storage: ${quotaGB.toFixed(1)}GB free, need ${MIN_QUOTA_GB.toFixed(1)}GB`)
}
}
/**
* Download and unpack an image, saving it to persistent storage.
*
* @param {ManifestImage} image
* @param {progressCallback} [onProgress]
* @returns {Promise<void>}
*/
async downloadImage(image, onProgress = undefined) {
const { archiveUrl, fileName } = image
/** @type {FileSystemWritableFileStream} */
let writable
try {
const fileHandle = await this.root.getFileHandle(fileName, { create: true })
writable = await fileHandle.createWritable()
} catch (e) {
throw new Error(`Error opening file handle: ${e}`, { cause: e })
}
console.debug(`[ImageManager] Downloading ${image.name} from ${archiveUrl}`)
let stream = await fetchStream(archiveUrl, { mode: 'cors' }, { onProgress })
try {
if (image.compressed) {
stream = new XzReadableStream(stream)
}
await stream.pipeTo(writable)
onProgress?.(1)
} catch (e) {
throw new Error(`Error unpacking archive: ${e}`, { cause: e })
}
}
/**
* Get a blob for an image.
*
* @param {ManifestImage} image
* @returns {Promise<Blob>}
*/
async getImage(image) {
const { fileName } = image
let fileHandle
try {
fileHandle = await this.root.getFileHandle(fileName, { create: false })
} catch (e) {
throw new Error(`Error getting file handle: ${e}`, { cause: e })
}
return fileHandle.getFile()
}
}
/** @returns {React.MutableRefObject<ImageManager>} */
export function useImageManager() {
const apiRef = useRef()
useEffect(() => {
const worker = new ImageManager()
apiRef.current = worker
}, [])
return apiRef
}