]> git.proxmox.com Git - proxmox-backup.git/blob - src/tape/drive/virtual_tape.rs
tape: remove MediaLabelInfo, use MediaId instead
[proxmox-backup.git] / src / tape / drive / virtual_tape.rs
1 // Note: This is only for test an debug
2
3 use std::fs::File;
4 use std::io;
5
6 use anyhow::{bail, format_err, Error};
7 use serde::{Serialize, Deserialize};
8
9 use proxmox::tools::{
10 fs::{replace_file, CreateOptions},
11 };
12
13 use crate::{
14 tape::{
15 TapeWrite,
16 TapeRead,
17 changer::MediaChange,
18 drive::{
19 VirtualTapeDrive,
20 TapeDriver,
21 },
22 file_formats::{
23 MediaSetLabel,
24 MediaContentHeader,
25 PROXMOX_BACKUP_MEDIA_SET_LABEL_MAGIC_1_0,
26 },
27 helpers::{
28 EmulateTapeReader,
29 EmulateTapeWriter,
30 BlockedReader,
31 BlockedWriter,
32 },
33 },
34 };
35
36 impl VirtualTapeDrive {
37
38 /// This needs to lock the drive
39 pub fn open(&self) -> Result<VirtualTapeHandle, Error> {
40 let mut lock_path = std::path::PathBuf::from(&self.path);
41 lock_path.push(".drive.lck");
42
43 let timeout = std::time::Duration::new(10, 0);
44 let lock = proxmox::tools::fs::open_file_locked(&lock_path, timeout, true)?;
45
46 Ok(VirtualTapeHandle {
47 _lock: lock,
48 max_size: self.max_size.unwrap_or(64*1024*1024),
49 path: std::path::PathBuf::from(&self.path),
50 })
51 }
52 }
53
54 #[derive(Serialize,Deserialize)]
55 struct VirtualTapeStatus {
56 name: String,
57 pos: usize,
58 }
59
60 #[derive(Serialize,Deserialize)]
61 struct VirtualDriveStatus {
62 current_tape: Option<VirtualTapeStatus>,
63 }
64
65 #[derive(Serialize,Deserialize)]
66 struct TapeIndex {
67 files: usize,
68 }
69
70 pub struct VirtualTapeHandle {
71 path: std::path::PathBuf,
72 max_size: usize,
73 _lock: File,
74 }
75
76 impl VirtualTapeHandle {
77
78 pub fn insert_tape(&self, _tape_filename: &str) {
79 unimplemented!();
80 }
81
82 pub fn eject_tape(&self) {
83 unimplemented!();
84 }
85
86 fn status_file_path(&self) -> std::path::PathBuf {
87 let mut path = self.path.clone();
88 path.push("drive-status.json");
89 path
90 }
91
92 fn tape_index_path(&self, tape_name: &str) -> std::path::PathBuf {
93 let mut path = self.path.clone();
94 path.push(format!("tape-{}.json", tape_name));
95 path
96 }
97
98 fn tape_file_path(&self, tape_name: &str, pos: usize) -> std::path::PathBuf {
99 let mut path = self.path.clone();
100 path.push(format!("tapefile-{}-{}.json", pos, tape_name));
101 path
102 }
103
104 fn load_tape_index(&self, tape_name: &str) -> Result<TapeIndex, Error> {
105 let path = self.tape_index_path(tape_name);
106 let raw = proxmox::tools::fs::file_get_contents(&path)?;
107 if raw.is_empty() {
108 return Ok(TapeIndex { files: 0 });
109 }
110 let data: TapeIndex = serde_json::from_slice(&raw)?;
111 Ok(data)
112 }
113
114 fn store_tape_index(&self, tape_name: &str, index: &TapeIndex) -> Result<(), Error> {
115 let path = self.tape_index_path(tape_name);
116 let raw = serde_json::to_string_pretty(&serde_json::to_value(index)?)?;
117
118 let options = CreateOptions::new();
119 replace_file(&path, raw.as_bytes(), options)?;
120 Ok(())
121 }
122
123 fn truncate_tape(&self, tape_name: &str, pos: usize) -> Result<usize, Error> {
124 let mut index = self.load_tape_index(tape_name)?;
125
126 if index.files <= pos {
127 return Ok(index.files)
128 }
129
130 for i in pos..index.files {
131 let path = self.tape_file_path(tape_name, i);
132 let _ = std::fs::remove_file(path);
133 }
134
135 index.files = pos;
136
137 self.store_tape_index(tape_name, &index)?;
138
139 Ok(index.files)
140 }
141
142 fn load_status(&self) -> Result<VirtualDriveStatus, Error> {
143 let path = self.status_file_path();
144
145 let default = serde_json::to_value(VirtualDriveStatus {
146 current_tape: None,
147 })?;
148
149 let data = proxmox::tools::fs::file_get_json(&path, Some(default))?;
150 let status: VirtualDriveStatus = serde_json::from_value(data)?;
151 Ok(status)
152 }
153
154 fn store_status(&self, status: &VirtualDriveStatus) -> Result<(), Error> {
155 let path = self.status_file_path();
156 let raw = serde_json::to_string_pretty(&serde_json::to_value(status)?)?;
157
158 let options = CreateOptions::new();
159 replace_file(&path, raw.as_bytes(), options)?;
160 Ok(())
161 }
162 }
163
164 impl TapeDriver for VirtualTapeHandle {
165
166 fn sync(&mut self) -> Result<(), Error> {
167 Ok(()) // do nothing for now
168 }
169
170 fn current_file_number(&mut self) -> Result<usize, Error> {
171 let status = self.load_status()
172 .map_err(|err| format_err!("current_file_number failed: {}", err.to_string()))?;
173
174 match status.current_tape {
175 Some(VirtualTapeStatus { pos, .. }) => { Ok(pos)},
176 None => bail!("current_file_number failed: drive is empty (no tape loaded)."),
177 }
178 }
179
180 fn read_next_file(&mut self) -> Result<Option<Box<dyn TapeRead>>, io::Error> {
181 let mut status = self.load_status()
182 .map_err(|err| io::Error::new(io::ErrorKind::Other, err.to_string()))?;
183
184 match status.current_tape {
185 Some(VirtualTapeStatus { ref name, ref mut pos }) => {
186
187 let index = self.load_tape_index(name)
188 .map_err(|err| io::Error::new(io::ErrorKind::Other, err.to_string()))?;
189
190 if *pos >= index.files {
191 return Ok(None); // EOM
192 }
193
194 let path = self.tape_file_path(name, *pos);
195 let file = std::fs::OpenOptions::new()
196 .read(true)
197 .open(path)?;
198
199 *pos += 1;
200 self.store_status(&status)
201 .map_err(|err| io::Error::new(io::ErrorKind::Other, err.to_string()))?;
202
203 let reader = Box::new(file);
204 let reader = Box::new(EmulateTapeReader::new(reader));
205
206 match BlockedReader::open(reader)? {
207 Some(reader) => Ok(Some(Box::new(reader))),
208 None => Ok(None),
209 }
210 }
211 None => proxmox::io_bail!("drive is empty (no tape loaded)."),
212 }
213 }
214
215 fn write_file(&mut self) -> Result<Box<dyn TapeWrite>, io::Error> {
216 let mut status = self.load_status()
217 .map_err(|err| io::Error::new(io::ErrorKind::Other, err.to_string()))?;
218
219 match status.current_tape {
220 Some(VirtualTapeStatus { ref name, ref mut pos }) => {
221
222 let mut index = self.load_tape_index(name)
223 .map_err(|err| io::Error::new(io::ErrorKind::Other, err.to_string()))?;
224
225 for i in *pos..index.files {
226 let path = self.tape_file_path(name, i);
227 let _ = std::fs::remove_file(path);
228 }
229
230 let mut used_space = 0;
231 for i in 0..*pos {
232 let path = self.tape_file_path(name, i);
233 used_space += path.metadata()?.len() as usize;
234
235 }
236 index.files = *pos + 1;
237
238 self.store_tape_index(name, &index)
239 .map_err(|err| io::Error::new(io::ErrorKind::Other, err.to_string()))?;
240
241 let path = self.tape_file_path(name, *pos);
242 let file = std::fs::OpenOptions::new()
243 .write(true)
244 .create(true)
245 .truncate(true)
246 .open(path)?;
247
248 *pos = index.files;
249
250 self.store_status(&status)
251 .map_err(|err| io::Error::new(io::ErrorKind::Other, err.to_string()))?;
252
253 let mut free_space = 0;
254 if used_space < self.max_size {
255 free_space = self.max_size - used_space;
256 }
257
258 let writer = Box::new(file);
259 let writer = Box::new(EmulateTapeWriter::new(writer, free_space));
260 let writer = Box::new(BlockedWriter::new(writer));
261
262 Ok(writer)
263 }
264 None => proxmox::io_bail!("drive is empty (no tape loaded)."),
265 }
266 }
267
268 fn move_to_eom(&mut self) -> Result<(), Error> {
269 let mut status = self.load_status()?;
270 match status.current_tape {
271 Some(VirtualTapeStatus { ref name, ref mut pos }) => {
272
273 let index = self.load_tape_index(name)
274 .map_err(|err| io::Error::new(io::ErrorKind::Other, err.to_string()))?;
275
276 *pos = index.files;
277 self.store_status(&status)
278 .map_err(|err| io::Error::new(io::ErrorKind::Other, err.to_string()))?;
279
280 Ok(())
281 }
282 None => bail!("drive is empty (no tape loaded)."),
283 }
284 }
285
286 fn rewind(&mut self) -> Result<(), Error> {
287 let mut status = self.load_status()?;
288 match status.current_tape {
289 Some(ref mut tape_status) => {
290 tape_status.pos = 0;
291 self.store_status(&status)?;
292 Ok(())
293 }
294 None => bail!("drive is empty (no tape loaded)."),
295 }
296 }
297
298 fn erase_media(&mut self, _fast: bool) -> Result<(), Error> {
299 let mut status = self.load_status()?;
300 match status.current_tape {
301 Some(VirtualTapeStatus { ref name, ref mut pos }) => {
302 *pos = self.truncate_tape(name, 0)?;
303 self.store_status(&status)?;
304 Ok(())
305 }
306 None => bail!("drive is empty (no tape loaded)."),
307 }
308 }
309
310 fn write_media_set_label(&mut self, media_set_label: &MediaSetLabel) -> Result<(), Error> {
311
312 let mut status = self.load_status()?;
313 match status.current_tape {
314 Some(VirtualTapeStatus { ref name, ref mut pos }) => {
315 *pos = self.truncate_tape(name, 1)?;
316 let pos = *pos;
317 self.store_status(&status)?;
318
319 if pos == 0 {
320 bail!("media is empty (no label).");
321 }
322 if pos != 1 {
323 bail!("write_media_set_label: truncate failed - got wrong pos '{}'", pos);
324 }
325
326 let raw = serde_json::to_string_pretty(&serde_json::to_value(media_set_label)?)?;
327 let header = MediaContentHeader::new(PROXMOX_BACKUP_MEDIA_SET_LABEL_MAGIC_1_0, raw.len() as u32);
328
329 {
330 let mut writer = self.write_file()?;
331 writer.write_header(&header, raw.as_bytes())?;
332 writer.finish(false)?;
333 }
334
335 Ok(())
336 }
337 None => bail!("drive is empty (no tape loaded)."),
338 }
339 }
340
341 fn eject_media(&mut self) -> Result<(), Error> {
342 let status = VirtualDriveStatus {
343 current_tape: None,
344 };
345 self.store_status(&status)
346 }
347 }
348
349 impl MediaChange for VirtualTapeHandle {
350
351 /// Try to load media
352 ///
353 /// We automatically create an empty virtual tape here (if it does
354 /// not exist already)
355 fn load_media(&mut self, label: &str) -> Result<(), Error> {
356 let name = format!("tape-{}.json", label);
357 let mut path = self.path.clone();
358 path.push(&name);
359 if !path.exists() {
360 eprintln!("unable to find tape {} - creating file {:?}", label, path);
361 let index = TapeIndex { files: 0 };
362 self.store_tape_index(label, &index)?;
363 }
364
365 let status = VirtualDriveStatus {
366 current_tape: Some(VirtualTapeStatus {
367 name: label.to_string(),
368 pos: 0,
369 }),
370 };
371 self.store_status(&status)
372 }
373
374 fn unload_media(&mut self) -> Result<(), Error> {
375 self.eject_media()?;
376 Ok(())
377 }
378
379 fn eject_on_unload(&self) -> bool {
380 true
381 }
382
383 fn list_media_changer_ids(&self) -> Result<Vec<String>, Error> {
384 let mut list = Vec::new();
385 for entry in std::fs::read_dir(&self.path)? {
386 let entry = entry?;
387 let path = entry.path();
388 if path.is_file() && path.extension() == Some(std::ffi::OsStr::new("json")) {
389 if let Some(name) = path.file_stem() {
390 if let Some(name) = name.to_str() {
391 if name.starts_with("tape-") {
392 list.push(name[5..].to_string());
393 }
394 }
395 }
396 }
397 }
398 Ok(list)
399 }
400 }
401
402 impl MediaChange for VirtualTapeDrive {
403
404 fn load_media(&mut self, changer_id: &str) -> Result<(), Error> {
405 let mut handle = self.open()?;
406 handle.load_media(changer_id)
407 }
408
409 fn unload_media(&mut self) -> Result<(), Error> {
410 let mut handle = self.open()?;
411 handle.eject_media()?;
412 Ok(())
413 }
414
415 fn eject_on_unload(&self) -> bool {
416 true
417 }
418
419 fn list_media_changer_ids(&self) -> Result<Vec<String>, Error> {
420 let handle = self.open()?;
421 handle.list_media_changer_ids()
422 }
423 }