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
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
// Copyright 2023 MaidSafe.net limited.
//
// This SAFE Network Software is licensed to you under The General Public License (GPL), version 3.
// Unless required by applicable law or agreed to in writing, the SAFE Network Software distributed
// under the GPL Licence is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. Please review the Licences for the specific language governing
// permissions and limitations relating to use of the SAFE Network Software.

use super::{
    file_system::{normalise_path_separator, upload_file_to_net},
    metadata::FileMeta,
    ProcessedFiles, RealPath,
};

use crate::{app::consts::*, Error, Result, Safe, XorUrl};

use serde::{Deserialize, Serialize};
use std::{collections::BTreeMap, fs, path::Path};
use tracing::{debug, info};

// To use for mapping files names (with path in a flattened hierarchy) to FileInfos
pub type FilesMap = BTreeMap<String, FileInfo>;

// Each FileInfo contains file metadata and the link to the file's XOR-URL
pub type FileInfo = BTreeMap<String, String>;

// Type of changes made to each item of a FilesMap
#[allow(clippy::large_enum_variant)]
#[derive(Debug, Serialize, Deserialize)]
pub enum FilesMapChange {
    Added(XorUrl),
    Updated(XorUrl),
    Removed(XorUrl),
    Failed(String),
}

impl FilesMapChange {
    pub fn is_success(&self) -> bool {
        match self {
            Self::Added(_) | Self::Updated(_) | Self::Removed(_) => true,
            Self::Failed(_) => false,
        }
    }

    pub fn link(&self) -> Option<&XorUrl> {
        match self {
            Self::Added(link) | Self::Updated(link) | Self::Removed(link) => Some(link),
            Self::Failed(_) => None,
        }
    }

    pub fn is_added(&self) -> bool {
        match self {
            Self::Added(_) => true,
            Self::Updated(_) | Self::Removed(_) | Self::Failed(_) => false,
        }
    }

    pub fn is_updated(&self) -> bool {
        match self {
            Self::Updated(_) => true,
            Self::Added(_) | Self::Removed(_) | Self::Failed(_) => false,
        }
    }

    pub fn is_removed(&self) -> bool {
        match self {
            Self::Removed(_) => true,
            Self::Added(_) | Self::Updated(_) | Self::Failed(_) => false,
        }
    }
}

// A trait to get an key attr and return an API Result
pub trait GetAttr {
    fn getattr(&self, key: &str) -> Result<&str>;
}

impl GetAttr for FileInfo {
    // Makes it more readable to conditionally get an attribute from a FileInfo
    // because we can call it in API funcs like fileitem.getattr("key")?;
    fn getattr(&self, key: &str) -> Result<&str> {
        match self.get(key) {
            Some(v) => Ok(v),
            None => Err(Error::EntryNotFound(format!("key not found: {key}"))),
        }
    }
}

// Helper function to add or update a FileInfo in a FilesMap
#[allow(clippy::too_many_arguments)]
pub(crate) async fn add_or_update_file_item(
    safe: &Safe,
    file_name: &Path,
    file_name_for_map: &str,
    file_path: &Path,
    file_meta: &FileMeta,
    file_link: Option<&str>,
    name_exists: bool,
    files_map: &mut FilesMap,
    processed_files: &mut ProcessedFiles,
) -> bool {
    // We need to add a new FileInfo, let's generate the FileInfo first
    match gen_new_file_item(safe, file_path, file_meta, file_link).await {
        Ok(new_file_item) => {
            // note: files have link property, dirs and symlinks do not
            let xorurl = new_file_item
                .get(PREDICATE_LINK)
                .unwrap_or(&String::default())
                .to_string();

            let file_item_change = if name_exists {
                FilesMapChange::Updated(xorurl)
            } else {
                FilesMapChange::Added(xorurl)
            };

            debug!("New FileInfo item: {:?}", new_file_item);
            debug!("New FileInfo item inserted as {:?}", file_name);
            files_map.insert(file_name_for_map.to_string(), new_file_item);

            processed_files.insert(file_name.to_path_buf(), file_item_change);

            true
        }
        Err(err) => {
            info!("Skipping file \"{}\": {:?}", file_link.unwrap_or(""), err);
            processed_files.insert(
                file_name.to_path_buf(),
                FilesMapChange::Failed(format!("{err}")),
            );

            false
        }
    }
}

// Generate a FileInfo for a file which can then be added to a FilesMap
async fn gen_new_file_item(
    safe: &Safe,
    file_path: &Path,
    file_meta: &FileMeta,
    link: Option<&str>, // must be symlink target or None if FileMeta::is_symlink() is true.
) -> Result<FileInfo> {
    let mut file_item = file_meta.to_file_item();
    if file_meta.is_file() {
        let xorurl = match link {
            None => upload_file_to_net(safe, file_path).await?,
            Some(link) => link.to_string(),
        };
        file_item.insert(PREDICATE_LINK.to_string(), xorurl);
    } else if file_meta.is_symlink() {
        // get metadata, with any symlinks resolved.
        let result = fs::metadata(file_path);
        let symlink_target_type = match result {
            Ok(meta) => {
                if meta.is_dir() {
                    "dir"
                } else {
                    "file"
                }
            }
            Err(_) => "unknown", // this occurs for a broken link.  on windows, this would be fixed by: https://github.com/rust-lang/rust/pull/47956
                                 // on unix, there is no way to know if broken link points to file or dir, though we could guess, based on if it has an extension or not.
        };
        let target_path = match link {
            Some(target) => target.to_string(),
            None => {
                let target_path = fs::read_link(file_path).map_err(|e| {
                    Error::FileSystemError(format!(
                        "Unable to read link: {}.  {:#?}",
                        file_path.display(),
                        e
                    ))
                })?;
                normalise_path_separator(&target_path.display().to_string())
            }
        };
        file_item.insert("symlink_target".to_string(), target_path);
        // This is a hint for windows-platform clients to be able to call
        //   symlink_dir() or symlink_file().  on unix, there's no need.
        file_item.insert(
            "symlink_target_type".to_string(),
            symlink_target_type.to_string(),
        );
    }

    Ok(file_item)
}

/// Returns a new `files_map` at the given path if the given path is a dir.
pub(crate) fn file_map_for_path(files_map: FilesMap, path: &str) -> Result<FilesMap> {
    let realpath = files_map.realpath(path)?;

    // evict symlinks or files
    if let Some(file_info) = files_map.get(&realpath) {
        let file_type = file_info.get("type").ok_or_else(|| {
            Error::ContentError(format!(
                "corrupt FileInfo: missing a \"type\" property at: {path}"
            ))
        })?;
        if FileMeta::filetype_is_symlink(file_type) {
            return Err(Error::ContentError(format!(
                "symlink should not be present in resolved real path: {realpath}"
            )));
        } else if FileMeta::filetype_is_file(file_type) {
            return Ok(files_map);
        }
        // else must be a directory, managed below
    }

    // chroot
    let chrooted_file_map = filesmap_chroot(&realpath, &files_map)?;
    Ok(chrooted_file_map)
}

/// If a file is found at path, returns its "link" (xorurl)
/// along with its metadata enriched with the name of the file
/// Else returns (None, None)
pub(crate) fn get_file_link_and_metadata(
    files_map: &FilesMap,
    path: &str,
) -> Result<(Option<String>, Option<FileInfo>)> {
    if path.is_empty() {
        return Ok((None, None));
    }

    let realpath = files_map.realpath(path)?;

    if let Some(file_info) = files_map.get(&realpath) {
        let file_type = file_info.get("type").ok_or_else(|| {
            Error::ContentError(format!(
                "corrupt FileInfo: missing a \"type\" property at: {path}",
            ))
        })?;

        if FileMeta::filetype_is_file(file_type) {
            // get link
            let link = file_info.get("link").ok_or_else(|| {
                Error::ContentError(format!(
                    "corrupt FileInfo: missing a \"link\" property at path: {path}",
                ))
            })?;

            // get FileInfo and enrich it with filename
            let mut enriched_file_info = (*file_info).clone();
            if let Some(filename) = Path::new(&path).file_name() {
                if let Some(name) = filename.to_str() {
                    enriched_file_info.insert("name".to_string(), name.to_owned());
                }
            }

            return Ok((Some(link.clone()), Some(enriched_file_info)));
        }
    }
    Ok((None, None))
}

fn filesmap_chroot(urlpath: &str, files_map: &FilesMap) -> Result<FilesMap> {
    let mut filtered_filesmap = FilesMap::default();
    let folder_path = if !urlpath.ends_with('/') {
        format!("{urlpath}/")
    } else {
        urlpath.to_string()
    };
    for (filepath, fileitem) in files_map.iter() {
        if filepath.starts_with(&folder_path) {
            let mut new_path = filepath.clone();
            new_path.replace_range(..folder_path.len(), "");
            filtered_filesmap.insert(new_path, fileitem.clone());
        }
    }

    if filtered_filesmap.is_empty() {
        Err(Error::ContentError(format!(
            "no data found for path: {folder_path}"
        )))
    } else {
        Ok(filtered_filesmap)
    }
}