Images Back End
Cover Page
DUE Wed, 03/20, 2 pm
We’ll start by learning how to configure the Chatter
backend to store images and videos. We will not be storing these in the PostgreSQL database itself, but rather each image/video will be stored in its own file. In the PostgreSQL database, we will store the URLs pointing to these files.
Install updates
Every time you ssh to your server, you will see something like:
N updates can be applied immediately.
if N
is not 0, run the following:
server$ sudo apt update
server$ sudo apt upgrade
If you see *** System restart required ***
when you ssh to your server, please run:
server$ sync
server$ sudo reboot
Your ssh session will be ended at the server. Wait a few minutes for the system to reboot before you ssh to your server again.
Modified Chatter
API data formats
In this lab, we allow users to post an image and/or a video clip with their chatt
. To that end, we will modify our back-end APIs and database to hold the new data.
As in the previous lab, the chatt
s retrieval API will send back all accumulated chatts in the form of a JSON array of string arrays. In addition to the three elements "username"
, "message"
, and "timestamp"
, each string array now carries two additional elements which are the URLs where the image and video data are stored on the server:
[
["username0", "message0", "id0", "timestamp0", "imageurl0", "videourl0"],
["username1", "message1", "id1", "timestamp1", "imageurl1", "videourl1"],
...
]
Each element of the array may have a value of JSON null
or the empty string (""
).
To post a chatt
, the client correspondingly sends a JSON object with keys "username"
, "message"
, "image"
, and "video"
. The value of each key is a string (not urls!). If a chatt
carries no image and/or video, since we’re using multipart/form-data
encoding instead of JSON, the key "image"
and/or "video"
may be omitted or their values may be set to the empty string (""
) or null
. For example:
{
"username": "YOUR_UNIQNAME",
"message": "Hello world!",
"image": "",
"video": ""
}
Database table
Add two new columns named imageurl
of type text
and videourl
also of type text
to the chatts
table in your chatterdb
database:
ALTER TABLE chatts ADD COLUMN imageurl TEXT, ADD COLUMN videourl TEXT;
If you’re uncertain as to how to work with PostgreSQL, please review the Audio
and Chatter
labs’ back-end specs.
Chatterd
back end mods
Due to the potentially large sizes of image and video files and to enable direct download of these files using image/video download libraries on the front end, both image and video data posted alongside chatt
is not stored in the PostgreSQL database. Instead the data is stored in the back-end filesystem. The image/video files are considered static files. We store them in a designated media directory in the back end, served by a static-file server. The filename of the image/video file is turned into a URL, and the URL is then stored in the PostgreSQL database alongside the chatt
’s username and message.
So as not to run up a bill on your cloud-based back end server, and for a bearable wait time when uploading, we will be limiting each image or video upload to 10 MB. On both Android and iOS front ends, the Google Photos
app is the easiest way to determine the size of your image and video files.
Create the media directory and set its access permissions:
server$ mkdir ~/441/chatterd/media
server$ chmod a+rx ~ ~/441 ~/441/chatterd ~/441/chatterd/media
server$ cd ~/441/chatterd
Go with Echo
Go with Echo
Editing handlers.go
Add the following libraries to the import
block at the top of the file:
import (
//...
"errors"
"io"
"os"
"path"
"sync"
//...
)
Now add two new properties to the Chatt
struct in handlers.go
:
type Chatt struct {
// . . .
ImageUrl *string `json:"imageUrl"`
VideoUrl *string `json:"videoUrl"`
}
Next add the postimages()
function along with the saveFormFile()
and copyZeroAlloc()
helper functions. We also specify our designated media directory to store image/video files.
const MEDIA_ROOT = "/home/ubuntu/441/chatterd/media/"
func postimages(c echo.Context) error {
var err error
var chatt Chatt
chatt.Username = string(c.FormValue("username"))
chatt.Message = string(c.FormValue("message"))
chatt.ImageUrl, err = saveFormFile(c, "image", chatt.Username, ".jpeg")
if err == nil {
chatt.VideoUrl, err = saveFormFile(c, "video", chatt.Username, ".mp4")
}
if err != nil {
return logClientErr(c, http.StatusUnprocessableEntity, err)
}
_, err = chatterDB.Exec(background, `INSERT INTO chatts (username, message, id, imageurl, videourl) VALUES ($1, $2, gen_random_uuid(), $3, $4)`, chatt.Username, chatt.Message, chatt.ImageUrl, chatt.VideoUrl)
if err != nil {
return logClientErr(c, http.StatusBadRequest, err)
}
logOk(c)
return c.JSON(http.StatusOK, "")
}
func saveFormFile(c echo.Context, media string, username string, ext string) (*string, error) {
content, err := c.FormFile(media)
if err != nil { return nil, nil } // not an error, media not posted
securename := path.Base(path.Clean(username))
if securename[0] == '.' || securename == "/" {
return nil, errors.New("invalid username")
}
filename := securename + "-" + strconv.FormatInt(time.Now().Unix(), 10) + ext
src, err := content.Open()
if err != nil { return nil, err }
defer src.Close()
dst, err := os.Create(MEDIA_ROOT + filename)
if err != nil { return nil, err }
defer dst.Close()
if _, err = copyZeroAlloc(dst, src); err != nil {
return nil, err
}
mediaUrl := "https://" + string(c.Request().Host) + "/media/" + filename
return &mediaUrl, nil
}
// adapted from "github.com/valyala/fasthttp"
var copyBufPool = sync.Pool{ New: func() any {return make([]byte, 16384) } }
func copyZeroAlloc(w io.Writer, r io.Reader) (int64, error) {
if directRead, ok := r.(io.WriterTo); ok {
return directRead.WriteTo(w)
}
if directWrite, ok := w.(io.ReaderFrom); ok {
return directWrite.ReadFrom(r)
}
// else copy in chunks of 16 KB.
vbuf := copyBufPool.Get()
buf := vbuf.([]byte)
n, err := io.CopyBuffer(w, r, buf)
copyBufPool.Put(vbuf)
return n, err
}
Make a copy of your getchatts()
in handlers.go
and name the copy getimages()
. In getimages()
, replace the SELECT
statement with the following: `SELECT username, message, id, time, imageurl, videourl FROM chatts ORDER BY time DESC`
. This statement will retrieve all data, including our new image and video URLs from the PostgreSQL database.
Still in getimages()
, replace the rows.Scan()
call in the for rows.Next() {}
block with:
rows.Scan(&chatt.Username, &chatt.Message, &chatt.Id, &chatt.Timestamp, &chatt.ImageUrl, &chatt.VideoUrl)
and if the returned err
is not nil
:
chattArr = append(chattArr, []any{chatt.Username, chatt.Message, chatt.Id, chatt.Timestamp, chatt.ImageUrl, chatt.VideoUrl})
In addition to the original three columns, we added reading the imageurl
and videourl
columns and included them in the chatt
data returned to the front end.
Save and exit handlers.go
.
Routing for new URLs
For the newly added getimages()
and postimages()
functions, add the following new routes to the routes
array in main.go
:
var routes = []Route {
// . . .
{"GET", "/getimages/", getimages},
{"POST", "/postimages/", postimages},
}
To serve image/video static files, in the main()
function add the following before launching the server:
server.Static("/media", MEDIA_ROOT)
server.Use(middleware.BodyLimit("10M"))
This tells Echo
to redirect URL path /media
to its static-file server and to serve media files from our designated media directory, whose value is stored in MEDIA_ROOT
, and to limit each upload to 10 MB.
Save and exit main.go
.
Go
is a compiled language, like C/C++ and unlike Python, which is an interpreted language. This means you must run go build
each and every time you made changes to your code, for the changes to show up in your executable.
Rebuild chatterd
:
server$ go get
server$ go build
and restart it:
server$ sudo systemctl restart chatterd
References
JavaScript with Express
JavaScript with Express
To support uploading of large media files, first install the multer
package as dependency:
server$ npm install multer
server$ npm install -D @types/multer
This should add to your package.json
, in the dependencies
block:
"multer": "^1.4.5-lts.1",
and in the devDependencies
block:
"@types/multer": "^1.4.12",
Editing handlers.ts
Add the postimages()
function:
export async function postimages(req: Request, res: Response) {
let chatt: Chatt
try {
chatt = req.body
} catch (error) {
res.status(422).json(error)
return
}
let imageurl: string | null
let videourl: string | null
try {
// https://github.com/expressjs/multer#usage
// https://www.typescriptlang.org/docs/handbook/advanced-types.html
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional_chaining#syntax
const files = req.files as { [fieldname: string]: Express.Multer.File[] }
const imagefilename = files['image']?.[0]['filename']
const videofilename = files.video?.[0].filename // alternate syntax, syntactic sugar?
imageurl = imagefilename ? `https://${req.hostname}/media/${imagefilename}` : null
videourl = videofilename ? `https://${req.hostname}/media/${videofilename}` : null
} catch (error) {
res.status(422).json(`${error}`)
return
}
try {
await chatterDB`INSERT INTO chatts (username, message, id, imageurl, videourl) VALUES (${chatt.username}, ${chatt.message}, ${randomUUID()}, ${imageurl}, ${videourl})`
res.json({})
} catch (error) {
res.status(400).json(`${error as PostgresError}`)
}
}
We also add postuser()
function to return the username
of a posted chatt
. We will use this in the next section to construct filenames for uploaded files:
export function postuser(req: Request): string | null {
let chatt: Chatt
try {
chatt = req.body
return chatt.username
} catch (error) {
return null
}
}
Finally, make a copy of your getchatts()
in handlers.ts
and name the copy getimages()
. In getimages()
, replace the declaration of chatts
with:
const chatts = await chatterDB`SELECT username, message, id, time, imageurl, videourl FROM chatts ORDER BY time DESC`.values()
This will retrieve all data, including our new image and video URLs from the PostgreSQL database.
Save and exit handlers.ts
.
Routing for new URLs
To serve image/video static files, add the following lines of import
at the top of main.ts
:
import { basename } from 'path'
import multer from 'multer'
We also specify our designated media directory to store image/video files, and maximum allowed media file size. Add the following lines to your main.ts
, before the try
block, right after the declaration of chatterDB
, for example:
const MEDIA_ROOT = '/home/ubuntu/441/chatterd/media'
const MEDIA_MXSZ = 10485760 // 10 MB, default is infinity
Next, we set up the multer
middleware. Inside your try
block, before the instantiation of the Express
app, add:
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, MEDIA_ROOT)
},
filename: (req, file, cb) => {
const username = handlers.postuser(req)
let securename: string
if (username) {
securename = basename(username)
} else {
securename = req.ip
}
const time = new Date(Date.now()).toISOString()
const filename = `${securename}-${time}.${basename(file.mimetype)}`
cb(null, filename)
}
})
const upload = multer({storage: storage, limits: {fileSize: MEDIA_MXSZ}})
We have set up multer
to store uploaded files to disk. The destination
directory for uploaded files is in MEDIA_ROOT
, which we defined earlier to be /home/ubuntu/441/media
. Each uploaded file will be given a file name of the form securename-uploadTime.mimeType
, where securename
is chatt.username
, with any path-looking part strips off, or the client’s IP address. We further limit the upload file size to MEDIA_MXSZ
, which we have previously defined to be 10 MB.
For the newly added getimages()
and postimages()
functions, add the following to the
Express
app
instantiation in main.ts
:
.get('/getimages/', handlers.getimages)
.post('/postimages', upload.fields([
{name: 'image', maxCount: 1},
{name: 'video', maxCount: 1}
]), handlers.postimages)
.use('/media', express.static(MEDIA_ROOT))
In addition to routing the two new APIs, the .use()
line tells Express
to redirect URL path /media
to serve media files from our designated media directory.
Save and exit main.ts
. Rebuild and restart chatterd
:
server$ npx tsc
server$ sudo systemctl restart chatterd
References
Python with Starlette
Python with Starlette
To support uploading of large media files, first install the following packages in your python’s virtual environment:
server$ source venv/bin/activate
(env):chatter$ pip install -U python-multipart werkzeug
Editing handlers.py
Add the following libraries to the import
block at the top of the file:
import os
from werkzeug.utils import secure_filename
Next add the postimages()
function along with the saveFormFile()
helper function.
We also specify our designated media directory to store image/video files, and maximum allowed media file size.
MEDIA_ROOT = '/home/ubuntu/441/chatterd/media/'
MEDIA_MXSZ = 10485760 # 10 MB
async def saveFormFile(fields, media, url, username, ext):
try:
file = fields[media]
if file.size > MEDIA_MXSZ:
# but the whole file will still be received, just not saved
raise BufferError
except KeyError:
return None # not an error, media not sent
except Exception:
raise
try:
if not (filename := secure_filename(username)):
raise NameError
filename = f'{filename}-{str(time.time())}{ext}'
filepath = os.path.join(MEDIA_ROOT, filename)
# open(): https://docs.python.org/3/library/functions.html#open
with open(filepath, 'wb') as f:
# write(): https://docs.python.org/3/tutorial/inputoutput.html#tut-files
# form.UploadFile.read(): https://www.starlette.io/requests/#request-files
f.write(await file.read(MEDIA_MXSZ))
f.close()
# url to string: https://stackoverflow.com/a/57514621/
return f'{url}{filename}'
except BaseException:
raise
async def postimages(request):
try:
url = str(request.url_for('media', path='/'))
# loading form-encoded data
async with request.form() as fields:
username = fields['username']
message = fields['message']
imageurl = await saveFormFile(fields, 'image', url, username, '.jpeg')
videourl = await saveFormFile(fields, 'video', url, username, '.mp4')
except BaseException as err:
print(f'{err=}')
return JSONResponse('Unprocessable entity', status_code=422)
try:
async with main.server.pool.connection() as connection:
async with connection.cursor() as cursor:
await cursor.execute('INSERT INTO chatts (username, message, id, imageurl, videourl) VALUES '
'(%s, %s, gen_random_uuid(), %s, %s);', (username, message, imageurl, videourl))
return JSONResponse({})
except StringDataRightTruncation as err:
print(f'Message too long: {str(err)}')
return JSONResponse(f'Message too long', status_code = 400)
except Exception as err:
print(f'{err=}')
return JSONResponse(f'{type(err).__name__}: {str(err)}', status_code = 500)
File size limit
Neither Starlette
, uvicorn
, nor Granian
support limiting maximum data upload size. In handlers.py
, we refuse to save an uploaded media file larger than MEDIA_MXSZ
, but this check happens after Starlette
has stored the upload as Python’s SpooledTemporaryFile
, potentially overflowing the server’s storage space.
Reverse proxies, such as Nginx, can put a limit on the maximum upload size, but enforcement seems to be limited to checking the content of HTTP requests’ Content-Length
header, which may not actually correspond to the real body size.
Make a copy of your getchatts()
in handlers.py
and name the copy getimages()
. In getimages()
, replace the SELECT
statement with the following: 'SELECT username, message, id, time, imageurl, videourl FROM chatts ORDER BY time DESC;'
. This statement will retrieve all data, including our new image and video URLs from the PostgreSQL database.
Save and exit handlers.py
.
Routing for new URLs
To serve image/video static files, add the StaticFiles
import and add the Mount
module to the existing import from starlette.routing
such that the lines near the top of the file reads:
from starlette.routing import Route, Mount
from starlette.staticfiles import StaticFiles
For the newly added getimages()
and postimages()
functions, add the following new routes to the routes
array in main.py
:
Route('/getimages/', handlers.getimages, methods=['GET']),
Route('/postimages/', handlers.postimages, methods=['POST']),
# static files: https://www.starlette.io/staticfiles
Mount('/media/', app=StaticFiles(directory=handlers.MEDIA_ROOT, follow_symlink=True), name='media'),
In addition to routing the two new APIs, the next line line tells Starlette
to redirect URL path /media
to serve media files from our designated media directory, whose value is stored in MEDIA_ROOT
in handlers.py
above.
Save and exit main.py
and restart chatterd
:
server$ sudo systemctl restart chatterd
References
Rust with axum
Rust with axum
Adding dependencies
In your package directory, ~/441/chatterd/
, open Cargo.toml
and replace the axum
line in the dependencies
block with:
axum = { version="0.8.0-alpha.1", features = ["multipart"] }
and add the following dependencies, replacing the tower-http
line with the one here that imports additional features:
futures = "0.3.30"
tokio-util = { version="0.7.12", features = ["io"] }
tower-http = { version = "0.6.1", features = ["fs", "limit", "trace"] }
Save and exit Cargo.toml
.
Editing handlers.rs
Add the following crates/modules in the use axum::{}
block at the top of the file:
use axum::{
body::Bytes,
// you can choose to "merge" the following with the existing
// `extract` module or add it as a separate line. Both work.
extract::{Host, Multipart},
// ...
};
and add the following as additional imported crates/modules:
use chrono::SecondsFormat;
use futures::{Stream, TryStreamExt};
use std::{
io,
io::{Error, ErrorKind},
path::{Path, PathBuf},
str,};
use tokio::{fs::File, io::{copy, BufWriter}};
use tokio_util::io::StreamReader;
use tower_http::BoxError;
As with the axum::extract
module, you can “merge” the chrono
and std
modules to their respective existing imports, or leave them as separate lines.
Next add the postimages()
function along with the saveFormFile()
and stream_to_file()
helper functions. We also specify our designated media directory to store image/video files.
pub const MEDIA_ROOT: &str = "/home/ubuntu/441/chatterd/media";
pub async fn postimages(
State(pgpool): State<PGPool>,
ConnectInfo(clientIP): ConnectInfo<SocketAddr>,
Host(host): Host,
mut form: Multipart,
) -> Result<Json<Value>, (StatusCode, String)> {
let mut username = String::new();
let mut message = String::new();
let mut imageurl: Option<String> = None;
let mut videourl: Option<String> = None;
let chatterDB = pgpool
.get()
.await
.map_err(|err| logServerErr(clientIP, err.to_string()))?;
while let Some(field) = form.next_field().await.unwrap_or(None) {
let ext = field
.content_type()
.map(|mimeType| mimeType[6..].to_string());
match &*field.name().unwrap_or_default() {
"username" => username.push_str(
str::from_utf8(&field.bytes().await.map_or(Bytes::from("unknown"), |s| s)).unwrap(),
),
"message" => message.push_str(
str::from_utf8(&field.bytes().await.map_or(Bytes::from(""), |s| s)).unwrap(),
),
"image" => {
imageurl = Some(
saveFormFile(field, &username, &host, &ext.unwrap_or("jpeg".to_string()))
.await
.map_err(|err| {
logClientErr(clientIP, StatusCode::UNPROCESSABLE_ENTITY, err.to_string())
})?,
)
}
"video" => {
videourl = Some(
saveFormFile(field, &username, &host, &ext.unwrap_or("mp4".to_string()))
.await
.map_err(|err| {
logClientErr(clientIP, StatusCode::UNPROCESSABLE_ENTITY, err.to_string())
})?,
)
}
&_ => unreachable!(),
}
}
chatterDB
.execute(
"INSERT INTO chatts (username, message, id, imageurl, videourl) VALUES ($1, $2, gen_random_uuid(), $3, $4)",
&[&username, &message, &imageurl, &videourl],
)
.await
.map_err(|err| logClientErr(clientIP, StatusCode::NOT_ACCEPTABLE, err.to_string()))?;
logOk(clientIP);
Ok(Json(json!({})))
}
async fn saveFormFile<S, E>(stream: S, username: &str, host: &str, ext: &str) -> io::Result<String>
where
S: Stream<Item = Result<Bytes, E>>,
E: Into<BoxError>,
{
let postTime = Utc::now().to_rfc3339_opts(SecondsFormat::Secs, true);
let securename = Path::new(username)
.file_stem()
.and_then(|stem| { stem.to_str() })
.ok_or_else(|| Error::from(ErrorKind::InvalidData))?;
let filename = &*format!("{securename}-{postTime}.{ext}");
stream_to_file(filename, stream)
.await
.map(|filename| format!("https://{host}/media/{filename}"))
}
// Save a `Stream` to a file
// from https://github.com/tokio-rs/axum/blob/main/examples/stream-to-file/src/main.rs
async fn stream_to_file<S, E>(filename: &str, stream: S) -> io::Result<&str>
where
S: Stream<Item = Result<Bytes, E>>,
E: Into<BoxError>,
{
let body_with_io_error = stream.map_err(|err| Error::new(ErrorKind::Other, err));
let body_reader = StreamReader::new(body_with_io_error);
futures::pin_mut!(body_reader);
let mut filepath = PathBuf::from(MEDIA_ROOT);
filepath.push(filename);
let mut file = BufWriter::new(
File::create(
filepath
.to_str()
.ok_or_else(|| Error::from(ErrorKind::PermissionDenied))?,
)
.await?,
);
// Copy the body into the file.
copy(&mut body_reader, &mut file).await?;
Ok(filename)
}
Make a copy of your getchatts()
in handlers.rs
and name the copy getimages()
. In getimages()
, replace the SELECT
statement with the following: "SELECT username, message, id, time, imageurl, videourl FROM chatts ORDER BY time DESC"
. This statement will retrieve all data, including our new image and video URLs from the PostgreSQL database.
Still in getimages()
, replace the chattArr.push(/*...*/);
line with:
chattArr.push(vec![
row.get("username"),
row.get("message"),
Some(row.get::<&str, Uuid>("id").to_string()),
Some(row.get::<&str, DateTime<Local>>("time").to_string()),
row.get("imageurl"),
row.get("videourl")
]);
In addition to the original three columns, which we now access by column name instead of by index, we added reading the imageurl
and videourl
columns and included them in the chatt
data returned to the front end.
Save and exit handlers.rs
.
Editing main.rs
Add the following crates/modules at the top of your src/main.rs
:
use axum::{
// you can choose to "merge" the following with the existing
// `extract` module or add it as a separate line. Both work.
// If your merge list has more than one elements, put both inside a pair of parantheses.
extract::DefaultBodyLimit,
handler::HandlerWithoutStateExt,
http::StatusCode,
// ...
};
then add the following crate:
use tower_http::{
limit::RequestBodyLimitLayer,
services::ServeDir,
};
For the newly added getimages()
and postimages()
functions, add the following to the Router
instantiation of the router
variable in main.rs
, right after the call to Router::new()
:
let router = Router::new() // look for this line and add the following right below the line
.route("/postimages/", post(handlers::postimages))
.layer(DefaultBodyLimit::disable()) // first disable default 2 MB limit
.layer(RequestBodyLimitLayer::new(10485760)) // then add 10 MB limit
.route("/getimages/", get(handlers::getimages))
.nest_service("/media",
ServeDir::new(handlers::MEDIA_ROOT)
.not_found_service(handle_not_found.into_service()))
For postimages()
, we limit the size of each uploaded media file to 10 MB. To set this limit, we must first disable the default limit of 2MB/upload.
In addition to routing the two new APIs, the .nest_service
line tells axum
to redirect URL path /media
to serve media files from our designated media directory, whose value is stored in handlers::MEDIA_ROOT
. If the requested file is not found at the MEDIA_ROOT
path, we call handle_not_found()
to inform the user:
async fn handle_not_found() -> (StatusCode, &'static str) {
(StatusCode::NOT_FOUND, "Not found")
}
Save and exit main.rs
. Rebuild and restart chatterd
:
server$ cargo build --release
server$ sudo systemctl restart chatterd
References
Testing image and video upload
To test your backend with Insomnia, configure the header and body of your POST
request like the following:
- Create
POST
request in Insomnia.If you have the Python/Django back end, be sure to include the trailing
/
inpostimages/
. Django cannot carry POSTed data when redirecting frompostimages
topostimages/
. -
In the dropdown menu from the
chatter
lab that you previously used to select JSON as the data format (under thePOST
dropdown), selectMultipart
instead. Enter a text value forusername
andmessage
as was done in thechatter
lab. Then add files forimage
andvideo
. First add “image” or “video” as key. Then click the arrow to the right of the value field. Choose “File” and upload your.jpeg
or.mp4
files (screenshot).If you want to send a
chatt
with no image and/or video, you can simply omit adding the key"image"
and/or"video"
. Or you may add these keys but set their values to either the empty string (""
) ornull
. In Insomnia, to set the value to the empty string, set the field type toText
in the drop-down arrow, to the right of the value field, which is then left empty. To set it tonull
, set the field type toFile
but don’t select any file. -
Click
Send
. -
Next do a
getimages/
request within Insomnia as you did in thechatter
lab. You should see the image and video URLs associated with yourchatt
above in the returned JSON.Stream error in HTTP/2 framing layer
Both the
getchatts/
andgetimages/
Chatter API specify an empty payload. If you send yourGET
request in Insomnia with a non-emptyMultipart
body and you’re running the Rust/axum server, the server will report,Error: Stream error in the HTTP/2 framing layer
.To correct this error, set the
GET
request body to empty in Insomia. Or you can ignore this error when you know it’s due to malformedGET
requests in Insomnia. Rust/axum server is more strict in reporting non-conformance. - You can copy the URLs returned and do another
GET
request on these URLs, allowing you to see the image/video in Insomnia.
And you’re done with the back end!
Submission guideline
- Update your repo with:
server$ cd ~/441/chatterd
If you have a Python-based back end, run:
server$ cp /etc/nginx/sites-enabled/chatterd etc-chatterd
In all cases, commit and push changes to your GitHub repo:
server$ git commit -am "images back end" server$ git push
- If
git push
fails due to new changes made to the remote repo, you will need to rungit pull
first. Then you may have to resolve any conflicts before you cangit push
again.
Once you are done with the back end, we’ll move on to the front end.
Prepared for EECS 441 by Wendan Jiang, Tianyi Zhao, Ollie Elmgren, Benjamin Brengman, Mark Wassink, Alexander Wu, Yibo Pi, and Sugih Jamin | Last updated: December 20th, 2024 |