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
//! `Imports` resource — bulk file/document import + export.
//!
//! Mirrors the Python SDK's `client.imports.*` surface.
//!
//! [`Imports::import_file`], [`Imports::import_document`], and
//! [`Imports::extract_document`] are `multipart/form-data` uploads;
//! [`Imports::export_file`] is a download.
use serde_json::{Map, Value};
use crate::error::Result;
use crate::http::HttpClient;
/// An in-memory file to upload as a multipart part.
///
/// Construct with [`FilePart::new`]; set an explicit MIME type with
/// [`FilePart::mime`] (defaults to `application/octet-stream`).
pub struct FilePart {
file_name: String,
bytes: Vec<u8>,
mime: String,
}
impl FilePart {
/// A file part with the given name + raw bytes (octet-stream).
pub fn new(file_name: impl Into<String>, bytes: Vec<u8>) -> Self {
Self {
file_name: file_name.into(),
bytes,
mime: "application/octet-stream".to_string(),
}
}
/// Override the part's MIME type (e.g. `application/pdf`).
pub fn mime(mut self, mime: impl Into<String>) -> Self {
self.mime = mime.into();
self
}
fn into_form(self, extra: Option<&Map<String, Value>>) -> Result<reqwest::multipart::Form> {
let part = reqwest::multipart::Part::bytes(self.bytes)
.file_name(self.file_name)
.mime_str(&self.mime)
.map_err(crate::error::AreevError::from)?;
let mut form = reqwest::multipart::Form::new().part("file", part);
if let Some(fields) = extra {
for (k, v) in fields {
let text = match v {
Value::String(s) => s.clone(),
other => other.to_string(),
};
form = form.text(k.clone(), text);
}
}
Ok(form)
}
}
/// File / document import + export.
///
/// Access via [`crate::Areev::imports`].
pub struct Imports<'a> {
http: &'a HttpClient,
memory_id: String,
}
impl<'a> Imports<'a> {
/// Internal constructor — use [`crate::Areev::imports`].
pub(crate) fn new(http: &'a HttpClient, memory_id: String) -> Self {
Self { http, memory_id }
}
fn p(&self, suffix: &str) -> String {
format!("/memories/{}/{}", self.memory_id, suffix)
}
/// Import a structured grain file (`.mg` / NDJSON) as a multipart
/// upload. `fields` are extra form fields. Emits an audit event.
pub async fn import_file(
&self,
file: FilePart,
fields: Option<&Map<String, Value>>,
) -> Result<Value> {
let form = file.into_form(fields)?;
self.http
._post_multipart(&self.p("import-file"), form)
.await
}
/// Import a document (PDF / DOCX / etc.) — the server extracts
/// grains. `fields` are extra form fields. Emits an audit event.
pub async fn import_document(
&self,
file: FilePart,
fields: Option<&Map<String, Value>>,
) -> Result<Value> {
let form = file.into_form(fields)?;
self.http
._post_multipart(&self.p("import-document"), form)
.await
}
/// Extract structured content from a document without storing it.
/// Read-only — nothing is persisted.
pub async fn extract_document(
&self,
file: FilePart,
fields: Option<&Map<String, Value>>,
) -> Result<Value> {
let form = file.into_form(fields)?;
self.http
._post_multipart(&self.p("extract-document"), form)
.await
}
/// Export the memory's grains as a downloadable file.
///
/// Data-portability operation (GDPR Art. 20) — always issued, never
/// gated.
///
/// The endpoint responds with a binary attachment
/// (`application/x-mg-stream` — a length-framed `.mg` blob, or NDJSON
/// for the JSON export format), **not** JSON, so this returns the raw
/// bytes verbatim. Persist them to a file or feed them to an `.mg`
/// reader; do **not** assume UTF-8 / JSON.
pub async fn export_file(
&self,
namespace: Option<&str>,
grain_type: Option<&str>,
) -> Result<Vec<u8>> {
let mut params = Map::new();
if let Some(n) = namespace {
params.insert("namespace".into(), Value::String(n.to_string()));
}
if let Some(g) = grain_type {
params.insert("grain_type".into(), Value::String(g.to_string()));
}
let query = if params.is_empty() {
None
} else {
Some(Value::Object(params))
};
self.http
._get_bytes(&self.p("export-file"), query.as_ref())
.await
}
}