]>
Commit | Line | Data |
---|---|---|
c4d8542e DM |
1 | //! Media Pool |
2 | //! | |
3 | //! A set of backup medias. | |
4 | //! | |
5 | //! This struct manages backup media state during backup. The main | |
7bb720cb | 6 | //! purpose is to allocate media sets and assing new tapes to it. |
c4d8542e DM |
7 | //! |
8 | //! | |
9 | ||
10 | use std::path::Path; | |
11 | use anyhow::{bail, Error}; | |
12 | use ::serde::{Deserialize, Serialize}; | |
13 | ||
14 | use proxmox::tools::Uuid; | |
15 | ||
16 | use crate::{ | |
8a0046f5 | 17 | backup::Fingerprint, |
c4d8542e DM |
18 | api2::types::{ |
19 | MediaStatus, | |
c1c2c8f6 | 20 | MediaLocation, |
c4d8542e DM |
21 | MediaSetPolicy, |
22 | RetentionPolicy, | |
23 | MediaPoolConfig, | |
24 | }, | |
25 | tools::systemd::time::compute_next_event, | |
26 | tape::{ | |
27 | MediaId, | |
28 | MediaSet, | |
c4d8542e | 29 | Inventory, |
c4d8542e | 30 | file_formats::{ |
a78348ac | 31 | MediaLabel, |
c4d8542e DM |
32 | MediaSetLabel, |
33 | }, | |
34 | } | |
35 | }; | |
36 | ||
9839d3f7 | 37 | /// Media Pool lock guard |
c4d8542e DM |
38 | pub struct MediaPoolLockGuard(std::fs::File); |
39 | ||
40 | /// Media Pool | |
41 | pub struct MediaPool { | |
42 | ||
43 | name: String, | |
44 | ||
45 | media_set_policy: MediaSetPolicy, | |
46 | retention: RetentionPolicy, | |
cdf39e62 DM |
47 | |
48 | changer_name: Option<String>, | |
ab77d660 | 49 | force_media_availability: bool, |
cdf39e62 | 50 | |
8a0046f5 | 51 | encrypt_fingerprint: Option<Fingerprint>, |
c4d8542e DM |
52 | |
53 | inventory: Inventory, | |
c4d8542e DM |
54 | |
55 | current_media_set: MediaSet, | |
56 | } | |
57 | ||
58 | impl MediaPool { | |
59 | ||
60 | /// Creates a new instance | |
cdf39e62 DM |
61 | /// |
62 | /// If you specify a `changer_name`, only media accessible via | |
63 | /// that changer is considered available. If you pass `None` for | |
64 | /// `changer`, all offline media is considered available (backups | |
65 | /// to standalone drives may not use media from inside a tape | |
66 | /// library). | |
c4d8542e DM |
67 | pub fn new( |
68 | name: &str, | |
69 | state_path: &Path, | |
70 | media_set_policy: MediaSetPolicy, | |
71 | retention: RetentionPolicy, | |
cdf39e62 | 72 | changer_name: Option<String>, |
8a0046f5 | 73 | encrypt_fingerprint: Option<Fingerprint>, |
c4d8542e DM |
74 | ) -> Result<Self, Error> { |
75 | ||
76 | let inventory = Inventory::load(state_path)?; | |
77 | ||
78 | let current_media_set = match inventory.latest_media_set(name) { | |
79 | Some(set_uuid) => inventory.compute_media_set_members(&set_uuid)?, | |
80 | None => MediaSet::new(), | |
81 | }; | |
82 | ||
c4d8542e DM |
83 | Ok(MediaPool { |
84 | name: String::from(name), | |
85 | media_set_policy, | |
86 | retention, | |
cdf39e62 | 87 | changer_name, |
c4d8542e | 88 | inventory, |
c4d8542e | 89 | current_media_set, |
8a0046f5 | 90 | encrypt_fingerprint, |
ab77d660 | 91 | force_media_availability: false, |
c4d8542e DM |
92 | }) |
93 | } | |
94 | ||
ab77d660 DM |
95 | /// Pretend all Online(x) and Offline media is available |
96 | /// | |
97 | /// Only media in Vault(y) is considered unavailable. | |
98 | pub fn force_media_availability(&mut self) { | |
99 | self.force_media_availability = true; | |
100 | } | |
101 | ||
90e16be3 DM |
102 | /// Returns the Uuid of the current media set |
103 | pub fn current_media_set(&self) -> &Uuid { | |
104 | self.current_media_set.uuid() | |
105 | } | |
106 | ||
c4d8542e DM |
107 | /// Creates a new instance using the media pool configuration |
108 | pub fn with_config( | |
c4d8542e DM |
109 | state_path: &Path, |
110 | config: &MediaPoolConfig, | |
cdf39e62 | 111 | changer_name: Option<String>, |
c4d8542e DM |
112 | ) -> Result<Self, Error> { |
113 | ||
e062ebbc | 114 | let allocation = config.allocation.clone().unwrap_or_else(|| String::from("continue")).parse()?; |
c4d8542e | 115 | |
e062ebbc | 116 | let retention = config.retention.clone().unwrap_or_else(|| String::from("keep")).parse()?; |
c4d8542e | 117 | |
8a0046f5 DM |
118 | let encrypt_fingerprint = match config.encrypt { |
119 | Some(ref fingerprint) => Some(fingerprint.parse()?), | |
120 | None => None, | |
121 | }; | |
122 | ||
123 | MediaPool::new( | |
124 | &config.name, | |
125 | state_path, | |
126 | allocation, | |
127 | retention, | |
cdf39e62 | 128 | changer_name, |
8a0046f5 DM |
129 | encrypt_fingerprint, |
130 | ) | |
c4d8542e DM |
131 | } |
132 | ||
133 | /// Returns the pool name | |
134 | pub fn name(&self) -> &str { | |
135 | &self.name | |
136 | } | |
137 | ||
8a0046f5 DM |
138 | /// Retruns encryption settings |
139 | pub fn encrypt_fingerprint(&self) -> Option<Fingerprint> { | |
140 | self.encrypt_fingerprint.clone() | |
141 | } | |
142 | ||
25350f33 DM |
143 | pub fn set_media_status_damaged(&mut self, uuid: &Uuid) -> Result<(), Error> { |
144 | self.inventory.set_media_status_damaged(uuid) | |
145 | } | |
8a0046f5 | 146 | |
c4d8542e DM |
147 | fn compute_media_state(&self, media_id: &MediaId) -> (MediaStatus, MediaLocation) { |
148 | ||
cfae8f06 | 149 | let (status, location) = self.inventory.status_and_location(&media_id.label.uuid); |
c4d8542e DM |
150 | |
151 | match status { | |
152 | MediaStatus::Full | MediaStatus::Damaged | MediaStatus::Retired => { | |
153 | return (status, location); | |
154 | } | |
155 | MediaStatus::Unknown | MediaStatus::Writable => { | |
156 | /* possibly writable - fall through to check */ | |
157 | } | |
158 | } | |
159 | ||
160 | let set = match media_id.media_set_label { | |
161 | None => return (MediaStatus::Writable, location), // not assigned to any pool | |
162 | Some(ref set) => set, | |
163 | }; | |
164 | ||
165 | if set.pool != self.name { // should never trigger | |
166 | return (MediaStatus::Unknown, location); // belong to another pool | |
167 | } | |
168 | if set.uuid.as_ref() == [0u8;16] { // not assigned to any pool | |
169 | return (MediaStatus::Writable, location); | |
170 | } | |
171 | ||
172 | if &set.uuid != self.current_media_set.uuid() { | |
173 | return (MediaStatus::Full, location); // assume FULL | |
174 | } | |
175 | ||
176 | // media is member of current set | |
177 | if self.current_media_set.is_last_media(&media_id.label.uuid) { | |
178 | (MediaStatus::Writable, location) // last set member is writable | |
179 | } else { | |
180 | (MediaStatus::Full, location) | |
181 | } | |
182 | } | |
183 | ||
184 | /// Returns the 'MediaId' with associated state | |
185 | pub fn lookup_media(&self, uuid: &Uuid) -> Result<BackupMedia, Error> { | |
186 | let media_id = match self.inventory.lookup_media(uuid) { | |
187 | None => bail!("unable to lookup media {}", uuid), | |
188 | Some(media_id) => media_id.clone(), | |
189 | }; | |
190 | ||
191 | if let Some(ref set) = media_id.media_set_label { | |
192 | if set.pool != self.name { | |
193 | bail!("media does not belong to pool ({} != {})", set.pool, self.name); | |
194 | } | |
195 | } | |
196 | ||
197 | let (status, location) = self.compute_media_state(&media_id); | |
198 | ||
199 | Ok(BackupMedia::with_media_id( | |
200 | media_id, | |
201 | location, | |
202 | status, | |
203 | )) | |
204 | } | |
205 | ||
206 | /// List all media associated with this pool | |
207 | pub fn list_media(&self) -> Vec<BackupMedia> { | |
208 | let media_id_list = self.inventory.list_pool_media(&self.name); | |
209 | ||
210 | media_id_list.into_iter() | |
211 | .map(|media_id| { | |
212 | let (status, location) = self.compute_media_state(&media_id); | |
213 | BackupMedia::with_media_id( | |
214 | media_id, | |
215 | location, | |
216 | status, | |
217 | ) | |
218 | }) | |
219 | .collect() | |
220 | } | |
221 | ||
222 | /// Set media status to FULL. | |
223 | pub fn set_media_status_full(&mut self, uuid: &Uuid) -> Result<(), Error> { | |
224 | let media = self.lookup_media(uuid)?; // check if media belongs to this pool | |
225 | if media.status() != &MediaStatus::Full { | |
cfae8f06 | 226 | self.inventory.set_media_status_full(uuid)?; |
c4d8542e DM |
227 | } |
228 | Ok(()) | |
229 | } | |
230 | ||
231 | /// Make sure the current media set is usable for writing | |
232 | /// | |
233 | /// If not, starts a new media set. Also creates a new | |
234 | /// set if media_set_policy implies it. | |
ab77d660 DM |
235 | /// |
236 | /// Note: We also call this in list_media to compute correct media | |
237 | /// status, so this must not change persistent/saved state. | |
90e16be3 DM |
238 | /// |
239 | /// Returns the reason why we started a new media set (if we do) | |
240 | pub fn start_write_session(&mut self, current_time: i64) -> Result<Option<String>, Error> { | |
241 | ||
242 | let mut create_new_set = match self.current_set_usable() { | |
243 | Err(err) => { | |
244 | Some(err.to_string()) | |
245 | } | |
246 | Ok(_) => None, | |
c4d8542e DM |
247 | }; |
248 | ||
90e16be3 | 249 | if create_new_set.is_none() { |
c4d8542e DM |
250 | match &self.media_set_policy { |
251 | MediaSetPolicy::AlwaysCreate => { | |
90e16be3 | 252 | create_new_set = Some(String::from("policy is AlwaysCreate")); |
c4d8542e DM |
253 | } |
254 | MediaSetPolicy::CreateAt(event) => { | |
255 | if let Some(set_start_time) = self.inventory.media_set_start_time(&self.current_media_set.uuid()) { | |
256 | if let Ok(Some(alloc_time)) = compute_next_event(event, set_start_time as i64, false) { | |
90e16be3 DM |
257 | if current_time >= alloc_time { |
258 | create_new_set = Some(String::from("policy CreateAt event triggered")); | |
c4d8542e DM |
259 | } |
260 | } | |
261 | } | |
262 | } | |
263 | MediaSetPolicy::ContinueCurrent => { /* do nothing here */ } | |
264 | } | |
265 | } | |
266 | ||
90e16be3 | 267 | if create_new_set.is_some() { |
c4d8542e | 268 | let media_set = MediaSet::new(); |
c4d8542e DM |
269 | self.current_media_set = media_set; |
270 | } | |
271 | ||
90e16be3 | 272 | Ok(create_new_set) |
c4d8542e DM |
273 | } |
274 | ||
275 | /// List media in current media set | |
276 | pub fn current_media_list(&self) -> Result<Vec<&Uuid>, Error> { | |
277 | let mut list = Vec::new(); | |
278 | for opt_uuid in self.current_media_set.media_list().iter() { | |
279 | match opt_uuid { | |
280 | Some(ref uuid) => list.push(uuid), | |
281 | None => bail!("current_media_list failed - media set is incomplete"), | |
282 | } | |
283 | } | |
284 | Ok(list) | |
285 | } | |
286 | ||
287 | // tests if the media data is considered as expired at sepcified time | |
288 | pub fn media_is_expired(&self, media: &BackupMedia, current_time: i64) -> bool { | |
289 | if media.status() != &MediaStatus::Full { | |
290 | return false; | |
291 | } | |
292 | ||
293 | let expire_time = self.inventory.media_expire_time( | |
294 | media.id(), &self.media_set_policy, &self.retention); | |
295 | ||
1bed3aed | 296 | current_time >= expire_time |
c4d8542e DM |
297 | } |
298 | ||
8a0046f5 DM |
299 | // check if a location is considered on site |
300 | pub fn location_is_available(&self, location: &MediaLocation) -> bool { | |
301 | match location { | |
cdf39e62 | 302 | MediaLocation::Online(name) => { |
ab77d660 DM |
303 | if self.force_media_availability { |
304 | true | |
cdf39e62 | 305 | } else { |
ab77d660 DM |
306 | if let Some(ref changer_name) = self.changer_name { |
307 | name == changer_name | |
308 | } else { | |
309 | // a standalone drive cannot use media currently inside a library | |
310 | false | |
311 | } | |
cdf39e62 DM |
312 | } |
313 | } | |
314 | MediaLocation::Offline => { | |
ab77d660 DM |
315 | if self.force_media_availability { |
316 | true | |
317 | } else { | |
318 | // consider available for standalone drives | |
319 | self.changer_name.is_none() | |
320 | } | |
cdf39e62 | 321 | } |
8a0046f5 DM |
322 | MediaLocation::Vault(_) => false, |
323 | } | |
324 | } | |
325 | ||
326 | fn add_media_to_current_set(&mut self, mut media_id: MediaId, current_time: i64) -> Result<(), Error> { | |
327 | ||
328 | let seq_nr = self.current_media_set.media_list().len() as u64; | |
329 | ||
330 | let pool = self.name.clone(); | |
331 | ||
332 | let encrypt_fingerprint = self.encrypt_fingerprint(); | |
333 | ||
334 | let set = MediaSetLabel::with_data( | |
335 | &pool, | |
336 | self.current_media_set.uuid().clone(), | |
337 | seq_nr, | |
338 | current_time, | |
339 | encrypt_fingerprint, | |
340 | ); | |
341 | ||
342 | media_id.media_set_label = Some(set); | |
343 | ||
344 | let uuid = media_id.label.uuid.clone(); | |
345 | ||
346 | let clear_media_status = true; // remove Full status | |
347 | self.inventory.store(media_id, clear_media_status)?; // store persistently | |
348 | ||
349 | self.current_media_set.add_media(uuid); | |
350 | ||
351 | Ok(()) | |
352 | } | |
353 | ||
354 | ||
c4d8542e DM |
355 | /// Allocates a writable media to the current media set |
356 | pub fn alloc_writable_media(&mut self, current_time: i64) -> Result<Uuid, Error> { | |
357 | ||
358 | let last_is_writable = self.current_set_usable()?; | |
359 | ||
c4d8542e DM |
360 | if last_is_writable { |
361 | let last_uuid = self.current_media_set.last_media_uuid().unwrap(); | |
362 | let media = self.lookup_media(last_uuid)?; | |
363 | return Ok(media.uuid().clone()); | |
364 | } | |
365 | ||
366 | // try to find empty media in pool, add to media set | |
367 | ||
8a0046f5 | 368 | let media_list = self.list_media(); |
c4d8542e DM |
369 | |
370 | let mut empty_media = Vec::new(); | |
8a0046f5 DM |
371 | let mut used_media = Vec::new(); |
372 | ||
373 | for media in media_list.into_iter() { | |
374 | if !self.location_is_available(media.location()) { | |
375 | continue; | |
376 | } | |
c4d8542e | 377 | // already part of a media set? |
8a0046f5 DM |
378 | if media.media_set_label().is_some() { |
379 | used_media.push(media); | |
380 | } else { | |
381 | // only consider writable empty media | |
382 | if media.status() == &MediaStatus::Writable { | |
383 | empty_media.push(media); | |
384 | } | |
c4d8542e | 385 | } |
c4d8542e DM |
386 | } |
387 | ||
8a0046f5 DM |
388 | // sort empty_media, newest first -> oldest last |
389 | empty_media.sort_unstable_by(|a, b| b.label().ctime.cmp(&a.label().ctime)); | |
c4d8542e | 390 | |
8a0046f5 | 391 | if let Some(media) = empty_media.pop() { |
c4d8542e | 392 | // found empty media, add to media set an use it |
8a0046f5 DM |
393 | let uuid = media.uuid().clone(); |
394 | self.add_media_to_current_set(media.into_id(), current_time)?; | |
395 | return Ok(uuid); | |
c4d8542e DM |
396 | } |
397 | ||
398 | println!("no empty media in pool, try to reuse expired media"); | |
399 | ||
400 | let mut expired_media = Vec::new(); | |
401 | ||
8a0046f5 | 402 | for media in used_media.into_iter() { |
c4d8542e DM |
403 | if let Some(set) = media.media_set_label() { |
404 | if &set.uuid == self.current_media_set.uuid() { | |
405 | continue; | |
406 | } | |
8a0046f5 DM |
407 | } else { |
408 | continue; | |
c4d8542e | 409 | } |
8a0046f5 | 410 | |
c4d8542e | 411 | if self.media_is_expired(&media, current_time) { |
8446fbca | 412 | println!("found expired media on media '{}'", media.label_text()); |
c4d8542e DM |
413 | expired_media.push(media); |
414 | } | |
415 | } | |
416 | ||
8a0046f5 DM |
417 | // sort expired_media, newest first -> oldest last |
418 | expired_media.sort_unstable_by(|a, b| { | |
419 | b.media_set_label().unwrap().ctime.cmp(&a.media_set_label().unwrap().ctime) | |
c4d8542e DM |
420 | }); |
421 | ||
8a0046f5 | 422 | if let Some(media) = expired_media.pop() { |
25e464c5 | 423 | println!("reuse expired media '{}'", media.label_text()); |
8a0046f5 DM |
424 | let uuid = media.uuid().clone(); |
425 | self.add_media_to_current_set(media.into_id(), current_time)?; | |
426 | return Ok(uuid); | |
25e464c5 | 427 | } |
c4d8542e | 428 | |
25e464c5 | 429 | println!("no expired media in pool, try to find unassigned/free media"); |
c4d8542e | 430 | |
25e464c5 DM |
431 | // try unassigned media |
432 | // fixme: lock free media pool to avoid races | |
433 | let mut free_media = Vec::new(); | |
434 | ||
435 | for media_id in self.inventory.list_unassigned_media() { | |
436 | ||
437 | let (status, location) = self.compute_media_state(&media_id); | |
438 | if media_id.media_set_label.is_some() { continue; } // should not happen | |
439 | ||
8a0046f5 DM |
440 | if !self.location_is_available(&location) { |
441 | continue; | |
c4d8542e | 442 | } |
25e464c5 DM |
443 | |
444 | // only consider writable media | |
445 | if status != MediaStatus::Writable { continue; } | |
446 | ||
447 | free_media.push(media_id); | |
448 | } | |
449 | ||
8a0046f5 DM |
450 | if let Some(media_id) = free_media.pop() { |
451 | println!("use free media '{}'", media_id.label.label_text); | |
452 | let uuid = media_id.label.uuid.clone(); | |
453 | self.add_media_to_current_set(media_id, current_time)?; | |
454 | return Ok(uuid); | |
c4d8542e | 455 | } |
25e464c5 | 456 | |
25e464c5 | 457 | bail!("alloc writable media in pool '{}' failed: no usable media found", self.name()); |
c4d8542e DM |
458 | } |
459 | ||
460 | /// check if the current media set is usable for writing | |
461 | /// | |
462 | /// This does several consistency checks, and return if | |
463 | /// the last media in the current set is in writable state. | |
464 | /// | |
465 | /// This return error when the media set must not be used any | |
466 | /// longer because of consistency errors. | |
467 | pub fn current_set_usable(&self) -> Result<bool, Error> { | |
468 | ||
6dd05135 DM |
469 | let media_list = self.current_media_set.media_list(); |
470 | ||
471 | let media_count = media_list.len(); | |
c4d8542e DM |
472 | if media_count == 0 { |
473 | return Ok(false); | |
474 | } | |
475 | ||
476 | let set_uuid = self.current_media_set.uuid(); | |
477 | let mut last_is_writable = false; | |
478 | ||
6dd05135 DM |
479 | let mut last_enc: Option<Option<Fingerprint>> = None; |
480 | ||
481 | for (seq, opt_uuid) in media_list.iter().enumerate() { | |
c4d8542e DM |
482 | let uuid = match opt_uuid { |
483 | None => bail!("media set is incomplete (missing media information)"), | |
484 | Some(uuid) => uuid, | |
485 | }; | |
486 | let media = self.lookup_media(uuid)?; | |
487 | match media.media_set_label() { | |
488 | Some(MediaSetLabel { seq_nr, uuid, ..}) if *seq_nr == seq as u64 && uuid == set_uuid => { /* OK */ }, | |
489 | Some(MediaSetLabel { seq_nr, uuid, ..}) if uuid == set_uuid => { | |
490 | bail!("media sequence error ({} != {})", *seq_nr, seq); | |
491 | }, | |
492 | Some(MediaSetLabel { uuid, ..}) => bail!("media owner error ({} != {}", uuid, set_uuid), | |
493 | None => bail!("media owner error (no owner)"), | |
494 | } | |
6dd05135 DM |
495 | |
496 | if let Some(set) = media.media_set_label() { // always true here | |
497 | if set.encryption_key_fingerprint != self.encrypt_fingerprint { | |
498 | bail!("pool encryption key changed"); | |
499 | } | |
500 | match last_enc { | |
501 | None => { | |
502 | last_enc = Some(set.encryption_key_fingerprint.clone()); | |
503 | } | |
504 | Some(ref last_enc) => { | |
505 | if last_enc != &set.encryption_key_fingerprint { | |
506 | bail!("inconsistent media encryption key"); | |
507 | } | |
508 | } | |
509 | } | |
510 | } | |
511 | ||
c4d8542e DM |
512 | match media.status() { |
513 | MediaStatus::Full => { /* OK */ }, | |
514 | MediaStatus::Writable if (seq + 1) == media_count => { | |
b81e37f6 DM |
515 | let media_location = media.location(); |
516 | if self.location_is_available(media_location) { | |
517 | last_is_writable = true; | |
518 | } else { | |
519 | if let MediaLocation::Vault(vault) = media_location { | |
c4d8542e DM |
520 | bail!("writable media offsite in vault '{}'", vault); |
521 | } | |
522 | } | |
523 | }, | |
524 | _ => bail!("unable to use media set - wrong media status {:?}", media.status()), | |
525 | } | |
526 | } | |
ab77d660 | 527 | |
c4d8542e DM |
528 | Ok(last_is_writable) |
529 | } | |
530 | ||
531 | /// Generate a human readable name for the media set | |
532 | pub fn generate_media_set_name( | |
533 | &self, | |
534 | media_set_uuid: &Uuid, | |
535 | template: Option<String>, | |
536 | ) -> Result<String, Error> { | |
537 | self.inventory.generate_media_set_name(media_set_uuid, template) | |
538 | } | |
539 | ||
540 | /// Lock the pool | |
541 | pub fn lock(base_path: &Path, name: &str) -> Result<MediaPoolLockGuard, Error> { | |
542 | let mut path = base_path.to_owned(); | |
543 | path.push(format!(".{}", name)); | |
544 | path.set_extension("lck"); | |
545 | ||
546 | let timeout = std::time::Duration::new(10, 0); | |
547 | let lock = proxmox::tools::fs::open_file_locked(&path, timeout, true)?; | |
548 | ||
549 | Ok(MediaPoolLockGuard(lock)) | |
550 | } | |
551 | } | |
552 | ||
553 | /// Backup media | |
554 | /// | |
555 | /// Combines 'MediaId' with 'MediaLocation' and 'MediaStatus' | |
556 | /// information. | |
557 | #[derive(Debug,Serialize,Deserialize,Clone)] | |
558 | pub struct BackupMedia { | |
559 | /// Media ID | |
560 | id: MediaId, | |
561 | /// Media location | |
562 | location: MediaLocation, | |
563 | /// Media status | |
564 | status: MediaStatus, | |
565 | } | |
566 | ||
567 | impl BackupMedia { | |
568 | ||
569 | /// Creates a new instance | |
570 | pub fn with_media_id( | |
571 | id: MediaId, | |
572 | location: MediaLocation, | |
573 | status: MediaStatus, | |
574 | ) -> Self { | |
575 | Self { id, location, status } | |
576 | } | |
577 | ||
578 | /// Returns the media location | |
579 | pub fn location(&self) -> &MediaLocation { | |
580 | &self.location | |
581 | } | |
582 | ||
583 | /// Returns the media status | |
584 | pub fn status(&self) -> &MediaStatus { | |
585 | &self.status | |
586 | } | |
587 | ||
588 | /// Returns the media uuid | |
589 | pub fn uuid(&self) -> &Uuid { | |
590 | &self.id.label.uuid | |
591 | } | |
592 | ||
593 | /// Returns the media set label | |
6543214d DM |
594 | pub fn media_set_label(&self) -> Option<&MediaSetLabel> { |
595 | self.id.media_set_label.as_ref() | |
596 | } | |
597 | ||
598 | /// Returns the media creation time | |
599 | pub fn ctime(&self) -> i64 { | |
600 | self.id.label.ctime | |
c4d8542e DM |
601 | } |
602 | ||
603 | /// Updates the media set label | |
604 | pub fn set_media_set_label(&mut self, set_label: MediaSetLabel) { | |
605 | self.id.media_set_label = Some(set_label); | |
606 | } | |
54f4ecd4 | 607 | |
c4d8542e | 608 | /// Returns the drive label |
a78348ac | 609 | pub fn label(&self) -> &MediaLabel { |
c4d8542e DM |
610 | &self.id.label |
611 | } | |
612 | ||
613 | /// Returns the media id (drive label + media set label) | |
614 | pub fn id(&self) -> &MediaId { | |
615 | &self.id | |
616 | } | |
617 | ||
8a0046f5 DM |
618 | /// Returns the media id, consumes self) |
619 | pub fn into_id(self) -> MediaId { | |
620 | self.id | |
621 | } | |
622 | ||
c4d8542e | 623 | /// Returns the media label (Barcode) |
8446fbca DM |
624 | pub fn label_text(&self) -> &str { |
625 | &self.id.label.label_text | |
c4d8542e DM |
626 | } |
627 | } |