Make image upload work in rust

Co-authored-by: Reimar <mail@reim.ar>
This commit is contained in:
Alexandertp 2024-09-10 15:44:50 +02:00
parent ce961e8703
commit f5258d42e4
13 changed files with 166 additions and 21 deletions

1
API/.gitignore vendored
View File

@ -1,3 +1,4 @@
database.sqlite3*
efbundle
appsettings*

View File

@ -1,17 +1,18 @@
import 'dart:io';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:flutter_image_compress/flutter_image_compress.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
import 'package:path/path.dart' as p;
enum ApiService {
auth,
app,
}
Future<String?> request(BuildContext? context, ApiService service, String method, String path, Object? body) async {
Future<String?> request(BuildContext? context, ApiService service, String method, String path, dynamic body) async {
final messenger = context != null ? ScaffoldMessenger.of(context) : null;
final prefs = await SharedPreferences.getInstance();
@ -42,7 +43,7 @@ Future<String?> request(BuildContext? context, ApiService service, String method
response = await function(
Uri.parse(host + path),
headers: headers,
body: body != null ? jsonEncode(body) : null,
body: body is Uint8List ? body : (body is Object ? jsonEncode(body) : null),
);
}
} catch (e) {
@ -103,7 +104,7 @@ Future<String?> putUser(
'ProfilePicture', // field name matches your backend DTO
fileStream,
length,
filename: profilePicture.path.split('/').last,
filename: p.basename(profilePicture.path),
);
request.files.add(multipartFile);
}

View File

@ -3,8 +3,9 @@ import 'dart:io';
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'package:mobile/base/sidemenu.dart';
import 'models.dart';
import 'models.dart' as models;
import 'api.dart' as api;
import 'package:path/path.dart' as path;
class CreateReviewPage extends StatefulWidget {
const CreateReviewPage({super.key});
@ -17,13 +18,13 @@ class CreateReviewPage extends StatefulWidget {
class _CreateReviewState extends State<CreateReviewPage> {
final titleInput = TextEditingController();
final contentInput = TextEditingController();
Place? place;
models.Place? place;
var rating = 0;
File? _selectedImage;
@override
Widget build(BuildContext context) {
place = ModalRoute.of(context)!.settings.arguments as Place;
place = ModalRoute.of(context)!.settings.arguments as models.Place;
return SideMenu(
selectedIndex: -1,
@ -111,6 +112,16 @@ class _CreateReviewState extends State<CreateReviewPage> {
}
Future<void> _submitReview() async {
models.Image? image;
if (_selectedImage != null) {
final fileName = path.basename(_selectedImage!.path);
final response = await api.request(context, api.ApiService.app, 'POST', '/images?file_name=$fileName', _selectedImage!.readAsBytesSync());
if (response == null) return;
image = models.Image.fromJson(jsonDecode(response));
}
final response = await api.request(context, api.ApiService.app, 'POST', '/reviews', {
'title': titleInput.text,
'content': contentInput.text,
@ -119,11 +130,12 @@ class _CreateReviewState extends State<CreateReviewPage> {
'rating': rating,
'lat': place!.point.latitude,
'lng': place!.point.longitude,
'image_id': image?.id,
});
if (response == null || !mounted) return;
final review = Review.fromJson(jsonDecode(response));
final review = models.Review.fromJson(jsonDecode(response));
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Review submitted')));

View File

@ -2,13 +2,13 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'package:mobile/models.dart';
import 'package:mobile/models.dart' as models;
import 'package:shared_preferences/shared_preferences.dart';
import 'api.dart' as api;
import 'base/variables.dart';
class EditProfilePage extends StatefulWidget {
final User? userData;
final models.User? userData;
const EditProfilePage({super.key, required this.userData});
@ -24,7 +24,7 @@ class _ProfilePageState extends State<EditProfilePage> {
TextEditingController confirmPasswordInput = TextEditingController();
File? _selectedImage;
set userData(User userData) {}
set userData(models.User userData) {}
@override

View File

@ -1,6 +1,6 @@
import 'package:flutter/material.dart';
import 'package:mobile/base/sidemenu.dart';
import 'package:mobile/models.dart';
import 'package:mobile/models.dart' as models;
import 'package:shared_preferences/shared_preferences.dart';
import 'package:google_fonts/google_fonts.dart';
import 'dart:convert';
@ -28,7 +28,7 @@ class _LoginPageState extends State<LoginPage> {
// Assuming token is a JSON string
Map<String, dynamic> json = jsonDecode(response);
Login jsonUser = Login.fromJson(json);
models.Login jsonUser = models.Login.fromJson(json);
final prefs = await SharedPreferences.getInstance();
prefs.setString('token', jsonUser.token);

View File

@ -103,6 +103,22 @@ class User {
}
}
class Image {
int id;
String userId;
String imageUrl;
Image(this.id, this.userId, this.imageUrl);
factory Image.fromJson(Map<String, dynamic> json) {
return Image(
json['id'],
json['user_id'],
json['image_url'],
);
}
}
class SearchResults {
LatLng location;
String name;

View File

@ -1,7 +1,7 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:mobile/base/variables.dart';
import 'package:mobile/models.dart';
import 'package:mobile/models.dart' as models;
import 'package:shared_preferences/shared_preferences.dart';
import 'base/sidemenu.dart';
import 'api.dart' as api;
@ -15,7 +15,7 @@ class ProfilePage extends StatefulWidget {
}
class _ProfilePageState extends State<ProfilePage> {
User? userData;
models.User? userData;
@override
void initState() {
@ -39,7 +39,7 @@ class _ProfilePageState extends State<ProfilePage> {
if (response == null) return;
Map<String, dynamic> json = jsonDecode(response);
User jsonUser = User.fromJson(json);
models.User jsonUser = models.User.fromJson(json);
setState(() {
userData = jsonUser;

View File

@ -33,6 +33,7 @@ dependencies:
http: ^1.2.1
flutter_map: ^7.0.2
latlong2: ^0.9.1
path: ^1.9.0
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.

View File

@ -1,7 +1,7 @@
JWT_SECRET=DenHerMåAldrigVæreOffentligKunIDetteDemoProjekt
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_SESSION_TOKEN=
AWS_ENDPOINT_URL=
AWS_REGION=
R2_BUCKET_NAME=
R2_BUCKET_URL=

View File

@ -243,6 +243,55 @@ version = "0.2.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f"
[[package]]
name = "anstream"
version = "0.6.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526"
dependencies = [
"anstyle",
"anstyle-parse",
"anstyle-query",
"anstyle-wincon",
"colorchoice",
"is_terminal_polyfill",
"utf8parse",
]
[[package]]
name = "anstyle"
version = "1.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1"
[[package]]
name = "anstyle-parse"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb"
dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-query"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a"
dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "anstyle-wincon"
version = "3.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8"
dependencies = [
"anstyle",
"windows-sys 0.52.0",
]
[[package]]
name = "async-trait"
version = "0.1.81"
@ -779,6 +828,12 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "colorchoice"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0"
[[package]]
name = "const-oid"
version = "0.9.6"
@ -973,6 +1028,29 @@ dependencies = [
"cfg-if",
]
[[package]]
name = "env_filter"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4f2c92ceda6ceec50f43169f9ee8424fe2db276791afde7b2cd8bc084cb376ab"
dependencies = [
"log",
"regex",
]
[[package]]
name = "env_logger"
version = "0.11.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e13fa619b91fb2381732789fc5de83b45675e882f66623b7d8cb4f643017018d"
dependencies = [
"anstream",
"anstyle",
"env_filter",
"humantime",
"log",
]
[[package]]
name = "equivalent"
version = "1.0.1"
@ -1276,6 +1354,12 @@ version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
[[package]]
name = "humantime"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
[[package]]
name = "hyper"
version = "0.14.30"
@ -1361,6 +1445,12 @@ version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3"
[[package]]
name = "is_terminal_polyfill"
version = "1.70.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
[[package]]
name = "itoa"
version = "1.0.11"
@ -2172,9 +2262,11 @@ dependencies = [
"actix-utils",
"actix-web",
"aws-config",
"aws-credential-types",
"aws-sdk-s3",
"base64 0.22.1",
"dotenvy",
"env_logger",
"hmac",
"refinery",
"reqwest",
@ -2539,6 +2631,12 @@ version = "2.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
[[package]]
name = "utf8parse"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "uuid"
version = "1.10.0"

View File

@ -4,6 +4,7 @@ version = "0.1.0"
edition = "2021"
[dependencies]
env_logger = "0.11"
base64 = "0.22.1"
sha2 = "0.10.8"
hmac = "0.12.1"
@ -18,4 +19,4 @@ tokio = { version = "1", features = ["full"] }
refinery = { version = "0.8.14", features = ["rusqlite"] }
rusqlite = { version = "0.31", features = ["bundled"] }
reqwest = { version = "0.11.16", features = ["blocking", "json"] }
aws-credential-types = { version = "1.2.1", features = ["hardcoded-credentials"] }

View File

@ -0,0 +1 @@
ALTER TABLE reviews ADD COLUMN image_id INT;

View File

@ -8,7 +8,10 @@ use models::{Favorite, Review, Image};
use serde::Deserialize;
use actix_web::web::Bytes;
use aws_sdk_s3::primitives::ByteStream;
use aws_sdk_s3::config::Region;
use rusqlite::types::Null;
use env_logger;
use aws_sdk_s3::config::Credentials;
mod embedded {
use refinery::embed_migrations;
@ -152,7 +155,7 @@ async fn create_review(auth: AuthorizedUser, data: web::Data<AppData>, input: we
let image_id = match input.image_id {
Some(image_id) => image_id.to_string(),
None => "NULL".to_string(),
}
};
match db.execute(
"INSERT INTO reviews (
@ -237,7 +240,14 @@ struct CreateImageQuery {
async fn create_image(auth: AuthorizedUser, data: web::Data<AppData>, bytes: Bytes, query: web::Query<CreateImageQuery>) -> impl Responder {
let db = data.database.lock().unwrap();
let config = aws_config::load_from_env().await;
let client = aws_sdk_s3::Client::new(&config);
println!("{:?}", config);
let s3_config = aws_sdk_s3::config::Builder::from(&config)
.force_path_style(true)
.build();
let client = aws_sdk_s3::Client::from_conf(s3_config);
let bucket_name = std::env::var("R2_BUCKET_NAME").expect("R2_BUCKET_NAME must be provided");
let bucket_url = std::env::var("R2_BUCKET_URL").expect("R2_BUCKET_URL must be provided");
@ -250,13 +260,14 @@ async fn create_image(auth: AuthorizedUser, data: web::Data<AppData>, bytes: Byt
.await;
if response.is_err() {
println!("{:?}", response.unwrap_err());
return HttpResponse::InternalServerError().finish();
}
let image_url = format!("{}/{}/{}", bucket_url, bucket_name, query.file_name);
match db.execute(
"INSERT INTO images (user_id, image_url)",
"INSERT INTO images (user_id, image_url) VALUES (:user_id, :image_url)",
&[
(":user_id", &auth.user_id),
(":image_url", &image_url),
@ -273,6 +284,8 @@ async fn create_image(auth: AuthorizedUser, data: web::Data<AppData>, bytes: Byt
#[actix_web::main]
async fn main() -> std::io::Result<()> {
env_logger::init();
let _ = dotenvy::dotenv();
let port = std::env::var("RUST_BACKEND_PORT")
@ -306,6 +319,7 @@ async fn main() -> std::io::Result<()> {
.service(reviews)
.service(create_review)
.service(delete_review)
.service(create_image)
})
.bind(("0.0.0.0", port))?
.run()