trackme_backends/strava/
uploads.rs

1// Copyright (C) Robin Krahl <robin.krahl@ireas.org>
2// SPDX-License-Identifier: GPL-3.0-or-later
3
4use std::{
5    fmt::{self, Display, Formatter},
6    fs::File,
7    path::Path,
8};
9
10use multipart::client::lazy::Multipart;
11use serde::Deserialize;
12
13use super::context::{AccessToken, Context};
14use crate::{Error, ErrorKind, Result};
15
16/// The options for a file upload.
17#[derive(Clone, Copy, Debug, Default)]
18pub struct UploadOptions<'a> {
19    /// The data type of the file.
20    ///
21    /// If not set, it is automatically determined from the file extension.
22    pub data_type: Option<DataType>,
23
24    /// The name of the activity.
25    ///
26    /// If not set, it is set automatically by Strava.
27    pub name: Option<&'a str>,
28
29    /// The description of the activity.
30    pub description: Option<&'a str>,
31
32    /// The sport type of the activity.
33    ///
34    /// If not set, it is automatically determined by Strava.
35    pub sport_type: Option<&'a str>,
36}
37
38#[derive(Clone, Copy, Debug)]
39pub enum DataType {
40    Fit,
41    FitGz,
42    Gpx,
43    GpxGz,
44    Tcx,
45    TcxGz,
46}
47
48impl DataType {
49    fn new(extension: &str, zipped: bool) -> Option<Self> {
50        let data_type = match (extension.to_ascii_lowercase().as_str(), zipped) {
51            ("fit", false) => Self::Fit,
52            ("fit", true) => Self::FitGz,
53            ("tcx", false) => Self::Tcx,
54            ("tcx", true) => Self::TcxGz,
55            ("gpx", false) => Self::Gpx,
56            ("gpx", true) => Self::GpxGz,
57            _ => {
58                return None;
59            }
60        };
61        Some(data_type)
62    }
63
64    pub fn guess(path: &Path) -> Option<Self> {
65        fn ext(path: &Path) -> Option<&str> {
66            path.extension()?.to_str()
67        }
68
69        let mut extension = ext(path)?;
70        let zipped = extension.eq_ignore_ascii_case("gz");
71        if zipped {
72            extension = ext(path.file_stem()?.as_ref())?;
73        }
74        Self::new(extension, zipped)
75    }
76
77    pub fn as_str(&self) -> &'static str {
78        match self {
79            Self::Fit => "fit",
80            Self::FitGz => "fit.gz",
81            Self::Tcx => "tcx",
82            Self::TcxGz => "tcx.gz",
83            Self::Gpx => "gpx",
84            Self::GpxGz => "gpx.gz",
85        }
86    }
87}
88
89impl Display for DataType {
90    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
91        self.as_str().fmt(f)
92    }
93}
94
95#[derive(Debug, Deserialize)]
96pub struct UploadResponse {
97    pub id: Option<i64>,
98    pub id_str: Option<String>,
99    pub external_id: Option<String>,
100    pub error: Option<String>,
101    pub status: Option<String>,
102    pub activity_id: Option<i64>,
103}
104
105pub fn create(
106    context: &Context<AccessToken>,
107    path: &Path,
108    options: UploadOptions<'_>,
109) -> Result<UploadResponse> {
110    let mut multipart = Multipart::new();
111    let file = File::open(path).map_err(|err| {
112        Error::new(
113            ErrorKind::FileReadingFailed {
114                path: path.to_owned(),
115            },
116            err,
117        )
118    })?;
119    multipart.add_stream(
120        "file",
121        file,
122        path.file_name().and_then(|s| s.to_str()),
123        None,
124    );
125    let data_type = options
126        .data_type
127        .or_else(|| DataType::guess(path))
128        .ok_or_else(|| ErrorKind::DataTypeParsingFailed {
129            filename: path.to_owned(),
130        })?;
131    multipart.add_text("data_type", data_type.as_str());
132    let options = [
133        ("name", &options.name),
134        ("description", &options.description),
135        ("sport_type", &options.sport_type),
136    ];
137    for (name, value) in options {
138        if let Some(value) = value {
139            multipart.add_text(name, *value);
140        }
141    }
142    context
143        .post_form("uploads", &mut multipart)?
144        .into_json()
145        .map_err(|err| Error::new(ErrorKind::ResponseFailed, err))
146}
147
148pub fn get(context: &Context<AccessToken>, id: i64) -> Result<UploadResponse> {
149    context
150        .get(&format!("uploads/{}", id))?
151        .into_json()
152        .map_err(|err| Error::new(ErrorKind::ResponseFailed, err))
153}