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
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
//! Tile storage implementations.
pub mod files;
pub mod mbtiles;
pub mod pmtiles;
pub mod s3;
pub mod s3putfiles;

use crate::config::{StoreCompressionCfg, TileStoreCfg};
use crate::mbtiles_ds::Error as MbtilesDsError;
use crate::store::files::FileStore;
use crate::store::mbtiles::MbtilesStore;
use crate::store::pmtiles::{PmtilesStoreReader, PmtilesStoreWriter};
use crate::store::s3::{S3Store, S3StoreError};
use async_trait::async_trait;
use bbox_core::config::error_exit;
use bbox_core::{Compression, Format, TileResponse};
use dyn_clone::{clone_trait_object, DynClone};
use log::warn;
use martin_mbtiles::{MbtError, Metadata};
use std::path::{Path, PathBuf};
use tile_grid::Xyz;

#[derive(thiserror::Error, Debug)]
pub enum TileStoreError {
    #[error("{0}: {1}")]
    FileError(PathBuf, #[source] std::io::Error),
    #[error("Missing argument: {0}")]
    ArgMissing(String),
    #[error("Operation not supported on readonly data store")]
    ReadOnly,
    #[error(transparent)]
    IoError(#[from] std::io::Error),
    #[error(transparent)]
    DbError(#[from] sqlx::Error),
    #[error(transparent)]
    S3StoreError(#[from] S3StoreError),
    #[error(transparent)]
    MbtilesDsError(#[from] MbtilesDsError),
    #[error(transparent)]
    MbtError(#[from] MbtError),
    #[error(transparent)]
    PmtilesError(#[from] ::pmtiles::error::Error),
}

#[async_trait]
pub trait TileWriter: DynClone + Send + Sync {
    /// Tile storage compression
    // TODO: move into common store trait?
    fn compression(&self) -> Compression;
    /// Check for existing tile
    /// Must not be implemented for cases where generating a tile is less expensive than checking
    // Method should probably return date of last change if known
    async fn exists(&self, xyz: &Xyz) -> bool;
    /// Write tile into store
    async fn put_tile(&self, xyz: &Xyz, data: Vec<u8>) -> Result<(), TileStoreError>;
    /// Write tile into store requiring &mut self
    async fn put_tile_mut(&mut self, xyz: &Xyz, data: Vec<u8>) -> Result<(), TileStoreError> {
        // Most implementations support writing without &mut self
        self.put_tile(xyz, data).await
    }
    /// Write multiple tiles into store
    async fn put_tiles(&mut self, tiles: &[(u8, u32, u32, Vec<u8>)]) -> Result<(), TileStoreError> {
        for (z, x, y, tile) in tiles {
            let _ = self
                .put_tile_mut(&Xyz::new(*x as u64, *y as u64, *z), tile.to_vec()) //FIXME: avoid clone!
                .await;
        }
        Ok(())
    }
    /// Finalize writing
    fn finalize(&mut self) -> Result<(), TileStoreError> {
        Ok(())
    }
}

clone_trait_object!(TileWriter);

#[async_trait]
pub trait TileReader: DynClone + Send + Sync {
    /// Lookup tile and return Read stream, if found
    async fn get_tile(&self, xyz: &Xyz) -> Result<Option<TileResponse>, TileStoreError>;
}

clone_trait_object!(TileReader);

#[derive(Clone, Debug)]
pub enum CacheLayout {
    Zxy,
}

impl CacheLayout {
    pub fn path(&self, base_dir: &Path, xyz: &Xyz, format: &Format) -> PathBuf {
        let mut path = base_dir.to_path_buf();
        match self {
            CacheLayout::Zxy => {
                // "{z}/{x}/{y}.{format}"
                path.push(xyz.z.to_string());
                path.push(xyz.x.to_string());
                path.push(xyz.y.to_string());
                path.set_extension(format.file_suffix());
            }
        }
        path
    }
    pub fn path_string(&self, base_dir: &Path, xyz: &Xyz, format: &Format) -> String {
        self.path(base_dir, xyz, format)
            .into_os_string()
            .to_string_lossy()
            .to_string()
    }
}

#[derive(Clone)]
pub struct NoStore;

#[async_trait]
impl TileWriter for NoStore {
    fn compression(&self) -> Compression {
        Compression::None
    }
    async fn exists(&self, _xyz: &Xyz) -> bool {
        false
    }
    async fn put_tile(&self, _xyz: &Xyz, _data: Vec<u8>) -> Result<(), TileStoreError> {
        Ok(())
    }
}

#[async_trait]
impl TileReader for NoStore {
    async fn get_tile(&self, _xyz: &Xyz) -> Result<Option<TileResponse>, TileStoreError> {
        Ok(None)
    }
}

pub async fn store_reader_from_config(
    config: &TileStoreCfg,
    compression: &Option<StoreCompressionCfg>,
    tileset_name: &str,
    format: &Format,
) -> Box<dyn TileReader> {
    match &config {
        TileStoreCfg::Files(cfg) => Box::new(FileStore::from_config(
            cfg,
            compression,
            tileset_name,
            format,
        )),
        TileStoreCfg::S3(cfg) => {
            Box::new(S3Store::from_config(cfg, compression, format).unwrap_or_else(error_exit))
        }
        TileStoreCfg::Mbtiles(cfg) => Box::new(
            MbtilesStore::from_config(cfg)
                .await
                .unwrap_or_else(error_exit),
        ),
        TileStoreCfg::Pmtiles(cfg) => {
            if let Ok(reader) = PmtilesStoreReader::from_config(cfg).await {
                Box::new(reader)
            } else {
                // We continue, because for seeding into a new file, the reader cannot be created and is not needed
                warn!(
                    "Couldn't open PmtilesStoreReader {}",
                    cfg.abs_path().display()
                );
                Box::new(NoStore)
            }
        }
        TileStoreCfg::NoStore => Box::new(NoStore),
    }
}

pub async fn store_writer_from_config(
    config: &TileStoreCfg,
    compression: &Option<StoreCompressionCfg>,
    tileset_name: &str,
    format: &Format,
    metadata: Metadata,
) -> Box<dyn TileWriter> {
    match &config {
        TileStoreCfg::Files(cfg) => Box::new(FileStore::from_config(
            cfg,
            compression,
            tileset_name,
            format,
        )),
        TileStoreCfg::S3(cfg) => {
            Box::new(S3Store::from_config(cfg, compression, format).unwrap_or_else(error_exit))
        }
        TileStoreCfg::Mbtiles(cfg) => Box::new(
            MbtilesStore::from_config_writable(cfg, metadata)
                .await
                .unwrap_or_else(error_exit),
        ),
        TileStoreCfg::Pmtiles(cfg) => {
            Box::new(PmtilesStoreWriter::from_config(cfg, metadata, format))
        }
        TileStoreCfg::NoStore => Box::new(NoStore),
    }
}