Skip to content

Commit 75be79c

Browse files
committed
initial commit
0 parents  commit 75be79c

26 files changed

+1885
-0
lines changed

.env.example

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
DB_PATH="./tubely.db"
2+
JWT_SECRET="JKFNDKAJSDKFASFNJWIROIOTNKNFDSKNFD"
3+
PLATFORM="dev"
4+
FILEPATH_ROOT="./app"
5+
ASSETS_ROOT="./assets"
6+
S3_BUCKET="tubely-123456789"
7+
S3_REGION="us-east-2"
8+
S3_CF_DISTRO="TEST"
9+
PORT="8091"
10+
# aws credentials should be set in ~/.aws/credentials
11+
# using the `aws configure` command, the SDK will automatically
12+
# read them from there

.gitignore

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
out
2+
*.db
3+
.env
4+
assets/
5+
samples/

README.md

+65
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# learn-file-storage-s3-golang-starter (Tubely)
2+
3+
This repo contains the starter code for the Tubely application - the #1 tool for engagement bait - for the "Learn File Servers and CDNs with S3 and CloudFront" [course](https://www.boot.dev/courses/learn-file-servers-s3-cloudfront-golang) on [boot.dev](https://www.boot.dev)
4+
5+
## Quickstart
6+
7+
*This is to be used as a *reference\* in case you need it, you should follow the instructions in the course rather than trying to do everything here.
8+
9+
## 1. Install dependencies
10+
11+
- [Go](https://golang.org/doc/install)
12+
- `go mod download` to download all dependencies
13+
- [FFMPEG](https://ffmpeg.org/download.html) - both `ffmpeg` and `ffprobe` are required to be in your `PATH`.
14+
15+
```bash
16+
# linux
17+
sudo apt update
18+
sudo apt install ffmpeg
19+
20+
# mac
21+
brew update
22+
brew install ffmpeg
23+
```
24+
25+
- [SQLite 3](https://www.sqlite.org/download.html) only required for you to manually inspect the database.
26+
27+
```bash
28+
# linux
29+
sudo apt update
30+
sudo apt install sqlite3
31+
32+
# mac
33+
brew update
34+
brew install sqlite3
35+
```
36+
37+
- [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html)
38+
39+
## 2. Download sample images and videos
40+
41+
```bash
42+
./samplesdownload.sh
43+
# samples/ dir will be created
44+
# with sample images and videos
45+
```
46+
47+
## 3. Configure environment variables
48+
49+
Copy the `.env.example` file to `.env` and fill in the values.
50+
51+
```bash
52+
cp .env.example .env
53+
```
54+
55+
You'll need to update values in the `.env` file to match your configuration, but _you won't need to do anything here until the course tells you to_.
56+
57+
## 3. Run the server
58+
59+
```bash
60+
go run .
61+
```
62+
63+
- You should see a new database file `tubely.db` created in the root directory.
64+
- You should see a new `assets` directory created in the root directory, this is where the images will be stored.
65+
- You should see a link in your console to open the local web page.

app/app.js

+301
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,301 @@
1+
document.addEventListener('DOMContentLoaded', async () => {
2+
const token = localStorage.getItem('token');
3+
4+
if (token) {
5+
document.getElementById('auth-section').style.display = 'none';
6+
document.getElementById('video-section').style.display = 'block';
7+
await getVideos();
8+
} else {
9+
document.getElementById('auth-section').style.display = 'block';
10+
document.getElementById('video-section').style.display = 'none';
11+
}
12+
});
13+
14+
document.getElementById('video-draft-form').addEventListener('submit', async (event) => {
15+
event.preventDefault();
16+
await createVideoDraft();
17+
});
18+
19+
document.getElementById('login-form').addEventListener('submit', async (event) => {
20+
event.preventDefault();
21+
await login();
22+
});
23+
24+
async function createVideoDraft() {
25+
const title = document.getElementById('video-title').value;
26+
const description = document.getElementById('video-description').value;
27+
28+
try {
29+
const res = await fetch('/api/videos', {
30+
method: 'POST',
31+
headers: {
32+
'Content-Type': 'application/json',
33+
Authorization: `Bearer ${localStorage.getItem('token')}`,
34+
},
35+
body: JSON.stringify({ title, description }),
36+
});
37+
const data = await res.json();
38+
if (!res.ok) {
39+
throw new Error(`Failed to create video draft: ${data.error}`);
40+
}
41+
42+
const videoID = data.id;
43+
if (videoID) {
44+
await getVideos();
45+
await videoStateHandler(videoID);
46+
}
47+
} catch (error) {
48+
alert(`Error: ${error.message}`);
49+
}
50+
}
51+
52+
async function login() {
53+
const email = document.getElementById('email').value;
54+
const password = document.getElementById('password').value;
55+
56+
try {
57+
const res = await fetch('/api/login', {
58+
method: 'POST',
59+
headers: {
60+
'Content-Type': 'application/json',
61+
},
62+
body: JSON.stringify({ email, password }),
63+
});
64+
const data = await res.json();
65+
if (!res.ok) {
66+
throw new Error(`Failed to login: ${data.error}`);
67+
}
68+
69+
if (data.token) {
70+
localStorage.setItem('token', data.token);
71+
document.getElementById('auth-section').style.display = 'none';
72+
document.getElementById('video-section').style.display = 'block';
73+
await getVideos();
74+
} else {
75+
alert('Login failed. Please check your credentials.');
76+
}
77+
} catch (error) {
78+
alert(`Error: ${error.message}`);
79+
}
80+
}
81+
82+
async function signup() {
83+
const email = document.getElementById('email').value;
84+
const password = document.getElementById('password').value;
85+
86+
try {
87+
const res = await fetch('/api/users', {
88+
method: 'POST',
89+
headers: {
90+
'Content-Type': 'application/json',
91+
},
92+
body: JSON.stringify({ email, password }),
93+
});
94+
if (!res.ok) {
95+
const data = await res.json();
96+
throw new Error(`Failed to create user: ${data.error}`);
97+
}
98+
console.log('User created!');
99+
await login();
100+
} catch (error) {
101+
alert(`Error: ${error.message}`);
102+
}
103+
}
104+
105+
function logout() {
106+
localStorage.removeItem('token');
107+
document.getElementById('auth-section').style.display = 'block';
108+
document.getElementById('video-section').style.display = 'none';
109+
}
110+
111+
function setUploadButtonState(uploading, selector) {
112+
const uploadBtn = document.getElementById(selector);
113+
if (uploading) {
114+
uploadBtn.textContent = 'Uploading...';
115+
uploadBtn.disabled = true;
116+
return;
117+
}
118+
uploadBtn.textContent = 'Upload';
119+
uploadBtn.disabled = false;
120+
}
121+
122+
async function uploadThumbnail(videoID) {
123+
const thumbnailFile = document.getElementById('thumbnail').files[0];
124+
if (!thumbnailFile) return;
125+
126+
const formData = new FormData();
127+
formData.append('thumbnail', thumbnailFile);
128+
129+
uploadBtnSelector = 'upload-thumbnail-btn';
130+
setUploadButtonState(true, uploadBtnSelector);
131+
132+
try {
133+
const res = await fetch(`/api/thumbnail_upload/${videoID}`, {
134+
method: 'POST',
135+
headers: {
136+
Authorization: `Bearer ${localStorage.getItem('token')}`,
137+
},
138+
body: formData,
139+
});
140+
if (!res.ok) {
141+
const data = await res.json();
142+
throw new Error(`Failed to upload thumbnail. Error: ${data.error}`);
143+
}
144+
145+
await res.json();
146+
console.log('Thumbnail uploaded!');
147+
await getVideo(videoID);
148+
} catch (error) {
149+
alert(`Error: ${error.message}`);
150+
}
151+
152+
setUploadButtonState(false, uploadBtnSelector);
153+
}
154+
155+
async function uploadVideoFile(videoID) {
156+
const videoFile = document.getElementById('video-file').files[0];
157+
if (!videoFile) return;
158+
159+
const formData = new FormData();
160+
formData.append('video', videoFile);
161+
162+
uploadBtnSelector = 'upload-video-btn';
163+
setUploadButtonState(true, uploadBtnSelector);
164+
165+
try {
166+
const res = await fetch(`/api/video_upload/${videoID}`, {
167+
method: 'POST',
168+
headers: {
169+
Authorization: `Bearer ${localStorage.getItem('token')}`,
170+
},
171+
body: formData,
172+
});
173+
if (!res.ok) {
174+
const data = await res.json();
175+
throw new Error(`Failed to upload video file. Error: ${data.error}`);
176+
}
177+
178+
console.log('Video uploaded!');
179+
await getVideo(videoID);
180+
} catch (error) {
181+
alert(`Error: ${error.message}`);
182+
}
183+
184+
setUploadButtonState(false, uploadBtnSelector);
185+
}
186+
187+
const videoStateHandler = createVideoStateHandler();
188+
189+
async function getVideos() {
190+
try {
191+
const res = await fetch('/api/videos', {
192+
method: 'GET',
193+
headers: {
194+
Authorization: `Bearer ${localStorage.getItem('token')}`,
195+
},
196+
});
197+
if (!res.ok) {
198+
const data = await res.json();
199+
throw new Error(`Failed to get videos. Error: ${data.error}`);
200+
}
201+
202+
const videos = await res.json();
203+
const videoList = document.getElementById('video-list');
204+
videoList.innerHTML = '';
205+
for (const video of videos) {
206+
const listItem = document.createElement('li');
207+
listItem.textContent = video.title;
208+
listItem.onclick = () => videoStateHandler(video.id);
209+
videoList.appendChild(listItem);
210+
}
211+
} catch (error) {
212+
alert(`Error: ${error.message}`);
213+
}
214+
}
215+
216+
function createVideoStateHandler() {
217+
let currentVideoID = null;
218+
219+
return async function handleVideoClick(videoID) {
220+
if (currentVideoID !== videoID) {
221+
currentVideoID = videoID;
222+
223+
// Reset file input values
224+
document.getElementById('thumbnail').value = '';
225+
document.getElementById('video-file').value = '';
226+
227+
await getVideo(videoID);
228+
}
229+
};
230+
}
231+
232+
async function getVideo(videoID) {
233+
try {
234+
const res = await fetch(`/api/videos/${videoID}`, {
235+
method: 'GET',
236+
headers: {
237+
Authorization: `Bearer ${localStorage.getItem('token')}`,
238+
},
239+
});
240+
if (!res.ok) {
241+
throw new Error('Failed to get video.');
242+
}
243+
244+
const video = await res.json();
245+
viewVideo(video);
246+
} catch (error) {
247+
alert(`Error: ${error.message}`);
248+
}
249+
}
250+
251+
let currentVideo = null;
252+
253+
function viewVideo(video) {
254+
currentVideo = video;
255+
document.getElementById('video-display').style.display = 'block';
256+
document.getElementById('video-title-display').textContent = video.title;
257+
document.getElementById('video-description-display').textContent = video.description;
258+
259+
const thumbnailImg = document.getElementById('thumbnail-image');
260+
if (!video.thumbnail_url) {
261+
thumbnailImg.style.display = 'none';
262+
} else {
263+
thumbnailImg.style.display = 'block';
264+
thumbnailImg.src = video.thumbnail_url;
265+
}
266+
267+
const videoPlayer = document.getElementById('video-player');
268+
if (videoPlayer) {
269+
if (!video.video_url) {
270+
videoPlayer.style.display = 'none';
271+
} else {
272+
videoPlayer.style.display = 'block';
273+
videoPlayer.src = video.video_url;
274+
videoPlayer.load();
275+
}
276+
}
277+
}
278+
279+
async function deleteVideo() {
280+
if (!currentVideo) {
281+
alert('No video selected for deletion.');
282+
return;
283+
}
284+
285+
try {
286+
const res = await fetch(`/api/videos/${currentVideo.id}`, {
287+
method: 'DELETE',
288+
headers: {
289+
Authorization: `Bearer ${localStorage.getItem('token')}`,
290+
},
291+
});
292+
if (!res.ok) {
293+
throw new Error('Failed to delete video.');
294+
}
295+
alert('Video deleted successfully.');
296+
document.getElementById('video-display').style.display = 'none';
297+
await getVideos();
298+
} catch (error) {
299+
alert(`Error: ${error.message}`);
300+
}
301+
}

0 commit comments

Comments
 (0)