Rust: с нуля до своего органайзера Урок 53 из 60
Резервная копия
Урок 53. Проверить резервную копию и восстановить данные в отдельный файл.
Текст переведён с помощью ИИ.
Повторите JSON и сохранение. Здесь используется учебная временная папка.
Образ и опорная карта
Запасной ключ полезен, только если он подходит к замку. Так же и копия файла полезна, только если её можно прочитать и проверить. Карта: основной файл → проверенная копия → повреждение основного → проверка копии → отдельный восстановленный файл. Исходный повреждённый файл остаётся для разбора. Образ не обещает, что копия защитит от удаления всей папки.
load превращает JSON в Organizer и проверяет версию, номера и названия. io::ErrorKind::InvalidData обозначает данные неверной формы или смысла. copy_checked сперва проверяет источник, затем создаёт новый целевой файл через create_new(true), пишет и синхронизирует, снова читает результат. create_new не перезапишет чужой файл. read_to_string потребует корректный UTF-8. В учебной папке primary, backup и recovered — три разных пути. fs::write портит только учебный основной файл, чтобы показать восстановление.
Запустите и разберите
Создайте проект с теми же зависимостями Serde, что в уроке 51. Замените src/main.rs первым Rust блоком и запустите cargo run. Проследите порядок вызовов: копию создают до повреждения; после повреждения читают копию, а восстановленный текст пишут в новый файл. Если проверка источника не прошла, целевой файл ещё не открыт. Ошибка записи может оставить неполный новый целевой файл; его нельзя принимать за годную копию без повторной проверки.
В Cargo.toml добавьте зависимости (строки ниже стоят после [package]):
В записи зависимости version = "=1.0.228" внутренний знак = требует именно эту версию, а features = ["derive"] включает возможность создавать реализации через derive; квадратные скобки здесь обозначают список возможностей. Это учебная фиксация версий, а не обещание вечной актуальности.
[dependencies]
serde = { version = "=1.0.228", features = ["derive"] }
serde_json = "=1.0.149"
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
use std::fs::{self, OpenOptions};
use std::io::{self, Write};
use std::path::Path;
#[derive(Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
struct Task {
id: u64,
title: String,
done: bool,
}
#[derive(Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
struct Organizer {
version: u32,
next_id: u64,
tasks: Vec<Task>,
}
fn load(text: &str) -> io::Result<Organizer> {
let data: Organizer = serde_json::from_str(text)
.map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))?;
if data.version != 1 || data.next_id == 0 {
return Err(io::Error::new(io::ErrorKind::InvalidData, "invalid header"));
}
let mut ids = HashSet::new();
for task in &data.tasks {
if task.id == 0
|| task.id >= data.next_id
|| task.title.trim().is_empty()
|| !ids.insert(task.id)
{
return Err(io::Error::new(io::ErrorKind::InvalidData, "invalid task"));
}
}
Ok(data)
}
fn copy_checked(source: &Path, target: &Path) -> io::Result<()> {
let text = fs::read_to_string(source)?;
load(&text)?;
let mut file = OpenOptions::new()
.write(true)
.create_new(true)
.open(target)?;
file.write_all(text.as_bytes())?;
file.sync_all()?;
drop(file);
load(&fs::read_to_string(target)?)?;
Ok(())
}
fn run() -> io::Result<()> {
let folder = std::env::temp_dir().join(format!("rust-lesson-53-{}", std::process::id()));
fs::create_dir(&folder)?;
let primary = folder.join("tasks.json");
let backup = folder.join("backup.json");
let recovered = folder.join("recovered.json");
let data = Organizer {
version: 1,
next_id: 2,
tasks: vec![Task {
id: 1,
title: String::from("Купить книгу"),
done: false,
}],
};
let json = serde_json::to_string(&data)
.map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))?;
fs::write(&primary, json)?;
copy_checked(&primary, &backup)?;
fs::write(&primary, "{broken")?;
println!(
"Основной файл повреждён: {}",
load(&fs::read_to_string(&primary)?).is_err()
);
copy_checked(&backup, &recovered)?;
let restored = load(&fs::read_to_string(&recovered)?)?;
println!("Восстановлено: {}", restored.tasks[0].title);
println!(
"Источник не изменён: {}",
fs::read_to_string(&primary)? == "{broken"
);
fs::remove_dir_all(folder)?;
Ok(())
}
fn main() {
if let Err(error) = run() {
eprintln!("Ошибка: {error}");
std::process::exit(1);
}
}
Основной файл повреждён: true
Восстановлено: Купить книгу
Источник не изменён: true
Вопросы для воспроизведения
- Почему сначала проверяют источник?
- Почему восстановление идёт в третий файл?
- Что нужно сделать, если повторное чтение копии не прошло?
Задание
Обязательное. Создайте отдельный файл с неверным JSON {broken, попробуйте copy_checked в новый путь и выведите, что ошибка обнаружена и восстановленный файл не появился. Не стирайте исправную копию.
Ответы
Показать ответы
Вызовите copy_checked(&bad_backup, &bad_target). Проверьте результат через is_err(), а отсутствие цели — через !bad_target.exists().
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
use std::fs::{self, OpenOptions};
use std::io::{self, Write};
use std::path::Path;
#[derive(Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
struct Task {
id: u64,
title: String,
done: bool,
}
#[derive(Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
struct Organizer {
version: u32,
next_id: u64,
tasks: Vec<Task>,
}
fn load(text: &str) -> io::Result<Organizer> {
let data: Organizer = serde_json::from_str(text)
.map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))?;
if data.version != 1 || data.next_id == 0 {
return Err(io::Error::new(io::ErrorKind::InvalidData, "invalid header"));
}
let mut ids = HashSet::new();
for task in &data.tasks {
if task.id == 0
|| task.id >= data.next_id
|| task.title.trim().is_empty()
|| !ids.insert(task.id)
{
return Err(io::Error::new(io::ErrorKind::InvalidData, "invalid task"));
}
}
Ok(data)
}
fn copy_checked(source: &Path, target: &Path) -> io::Result<()> {
let text = fs::read_to_string(source)?;
load(&text)?;
let mut file = OpenOptions::new()
.write(true)
.create_new(true)
.open(target)?;
file.write_all(text.as_bytes())?;
file.sync_all()?;
drop(file);
load(&fs::read_to_string(target)?)?;
Ok(())
}
fn run() -> io::Result<()> {
let folder = std::env::temp_dir().join(format!("rust-lesson-53-{}", std::process::id()));
fs::create_dir(&folder)?;
let primary = folder.join("tasks.json");
let backup = folder.join("backup.json");
let recovered = folder.join("recovered.json");
let data = Organizer {
version: 1,
next_id: 2,
tasks: vec![Task {
id: 1,
title: String::from("Купить книгу"),
done: false,
}],
};
let json = serde_json::to_string(&data)
.map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))?;
fs::write(&primary, json)?;
copy_checked(&primary, &backup)?;
fs::write(&primary, "{broken")?;
println!(
"Основной файл повреждён: {}",
load(&fs::read_to_string(&primary)?).is_err()
);
copy_checked(&backup, &recovered)?;
let restored = load(&fs::read_to_string(&recovered)?)?;
println!("Восстановлено: {}", restored.tasks[0].title);
println!(
"Источник не изменён: {}",
fs::read_to_string(&primary)? == "{broken"
);
let damaged_backup = folder.join("damaged-backup.json");
let other = folder.join("other-recovery.json");
fs::write(&damaged_backup, "{broken")?;
let rejected = copy_checked(&damaged_backup, &other).is_err() && !other.exists();
println!("Плохая копия отклонена: {}", rejected);
fs::remove_dir_all(folder)?;
Ok(())
}
fn main() {
if let Err(error) = run() {
eprintln!("Ошибка: {error}");
std::process::exit(1);
}
}
Основной файл повреждён: true
Восстановлено: Купить книгу
Источник не изменён: true
Плохая копия отклонена: true
После проверки
Проверенная копия не заменяет стратегию хранения: она может устареть или исчезнуть вместе с основным файлом. Перед восстановлением покажите человеку, какую версию данных вы нашли. Вернитесь к уроку 51. Документация create_new.
Если вы нашли ошибку или опечатку в тексте статьи, то сообщите нам об этом
Комментарии (0)
Войдите, чтобы оставить комментарий →
Пока нет комментариев. Будьте первым.