trackme_backends/strava/
uploads.rs1use 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#[derive(Clone, Copy, Debug, Default)]
18pub struct UploadOptions<'a> {
19 pub data_type: Option<DataType>,
23
24 pub name: Option<&'a str>,
28
29 pub description: Option<&'a str>,
31
32 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}