Merge branch 'fileupload' into 'master'

Video upload through webpage

Closes #59

See merge request lukas/openmediacenter!55
This commit is contained in:
Lukas Heiligenbrunner 2021-09-26 20:46:21 +00:00
commit b685b7d7be
13 changed files with 266 additions and 21 deletions

View File

@ -45,6 +45,8 @@ module.exports = {
// Map from global var to bool specifying if it can be redefined // Map from global var to bool specifying if it can be redefined
globals: { globals: {
File: true,
FileList: true,
jest: true, jest: true,
__DEV__: true, __DEV__: true,
__dirname: false, __dirname: false,

View File

@ -6,4 +6,5 @@ func AddHandlers() {
addTagHandlers() addTagHandlers()
addActorsHandlers() addActorsHandlers()
addTvshowHandlers() addTvshowHandlers()
addUploadHandler()
} }

70
apiGo/api/FileUpload.go Normal file
View File

@ -0,0 +1,70 @@
package api
import (
"fmt"
"io"
"openmediacenter/apiGo/api/api"
"openmediacenter/apiGo/database"
"openmediacenter/apiGo/videoparser"
"os"
"path/filepath"
)
func addUploadHandler() {
api.AddHandler("fileupload", api.VideoNode, api.PermUser, func(ctx api.Context) {
// get path where to store videos to
mSettings, PathPrefix, _ := database.GetSettings()
req := ctx.GetRequest()
mr, err := req.MultipartReader()
if err != nil {
ctx.Errorf("incorrect request!")
return
}
videoparser.InitDeps(&mSettings)
for {
part, err := mr.NextPart()
if err == io.EOF {
break
}
// todo allow more video formats than mp4
// only allow valid extensions
if filepath.Ext(part.FileName()) != ".mp4" {
continue
}
vidpath := PathPrefix + mSettings.VideoPath + part.FileName()
dst, err := os.OpenFile(vidpath, os.O_WRONLY|os.O_CREATE, 0644)
if err != nil {
ctx.Error("error opening file")
return
}
fmt.Printf("Uploading file %s\n", part.FileName())
// so now loop through every appended file and upload
buffer := make([]byte, 100000)
for {
cBytes, err := part.Read(buffer)
if cBytes > 0 {
dst.Write(buffer[0:cBytes])
}
if err == io.EOF {
fmt.Printf("Finished uploading file %s\n", part.FileName())
go videoparser.ProcessVideo(part.FileName())
break
}
}
_ = dst.Close()
}
ctx.Json(struct {
Message string
}{Message: "finished all files"})
})
}

View File

@ -15,7 +15,7 @@ const (
LoginNode = "login" LoginNode = "login"
) )
func AddHandler(action string, apiNode string, perm uint8, handler func(ctx Context)) { func AddHandler(action string, apiNode string, perm Perm, handler func(ctx Context)) {
http.Handle(fmt.Sprintf("/api/%s/%s", apiNode, action), http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { http.Handle(fmt.Sprintf("/api/%s/%s", apiNode, action), http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
srvPwd := settings.GetPassword() srvPwd := settings.GetPassword()
if srvPwd == nil { if srvPwd == nil {

View File

@ -13,12 +13,18 @@ import (
var srv *server.Server var srv *server.Server
type Perm uint8
const ( const (
PermAdmin uint8 = iota PermAdmin Perm = iota
PermUser uint8 = iota PermUser
PermUnauthorized uint8 = iota PermUnauthorized
) )
func (p Perm) String() string {
return [...]string{"PermAdmin", "PermUser", "PermUnauthorized"}[p]
}
const SignKey = "89013f1753a6890c6090b09e3c23ff43" const SignKey = "89013f1753a6890c6090b09e3c23ff43"
const TokenExpireHours = 24 const TokenExpireHours = 24
@ -27,7 +33,7 @@ type Token struct {
ExpiresAt int64 ExpiresAt int64
} }
func TokenValid(token string) (int, uint8) { func TokenValid(token string) (int, Perm) {
t, err := jwt.ParseWithClaims(token, &jwt.StandardClaims{}, func(token *jwt.Token) (interface{}, error) { t, err := jwt.ParseWithClaims(token, &jwt.StandardClaims{}, func(token *jwt.Token) (interface{}, error) {
return []byte(SignKey), nil return []byte(SignKey), nil
}) })
@ -42,7 +48,7 @@ func TokenValid(token string) (int, uint8) {
if err != nil { if err != nil {
return -1, PermUnauthorized return -1, PermUnauthorized
} }
return id, uint8(permid) return id, Perm(permid)
} }
func InitOAuth() { func InitOAuth() {

View File

@ -13,6 +13,7 @@ type Context interface {
GetRequest() *http.Request GetRequest() *http.Request
GetWriter() http.ResponseWriter GetWriter() http.ResponseWriter
UserID() int UserID() int
PermID() Perm
} }
type apicontext struct { type apicontext struct {
@ -20,7 +21,7 @@ type apicontext struct {
request *http.Request request *http.Request
responseWritten bool responseWritten bool
userid int userid int
permid uint8 permid Perm
} }
func (r *apicontext) GetRequest() *http.Request { func (r *apicontext) GetRequest() *http.Request {
@ -58,3 +59,7 @@ func (r *apicontext) Error(msg string) {
func (r *apicontext) Errorf(msg string, args ...interface{}) { func (r *apicontext) Errorf(msg string, args ...interface{}) {
r.Error(fmt.Sprintf(msg, &args)) r.Error(fmt.Sprintf(msg, &args))
} }
func (r *apicontext) PermID() Perm {
return r.permid
}

View File

@ -4,6 +4,7 @@ import (
"database/sql" "database/sql"
"fmt" "fmt"
"openmediacenter/apiGo/api/types" "openmediacenter/apiGo/api/types"
"openmediacenter/apiGo/config"
"openmediacenter/apiGo/database" "openmediacenter/apiGo/database"
"openmediacenter/apiGo/videoparser/tmdb" "openmediacenter/apiGo/videoparser/tmdb"
"regexp" "regexp"
@ -11,7 +12,7 @@ import (
"strings" "strings"
) )
var mSettings types.SettingsType var mSettings *types.SettingsType
var mExtDepsAvailable *ExtDependencySupport var mExtDepsAvailable *ExtDependencySupport
// default Tag ids // default Tag ids
@ -32,20 +33,22 @@ type VideoAttributes struct {
Width uint Width uint
} }
func ReIndexVideos(path []string, sett types.SettingsType) { func InitDeps(sett *types.SettingsType) {
mSettings = sett mSettings = sett
// check if the extern dependencies are available // check if the extern dependencies are available
mExtDepsAvailable = checkExtDependencySupport() mExtDepsAvailable = checkExtDependencySupport()
fmt.Printf("FFMPEG support: %t\n", mExtDepsAvailable.FFMpeg) fmt.Printf("FFMPEG support: %t\n", mExtDepsAvailable.FFMpeg)
fmt.Printf("MediaInfo support: %t\n", mExtDepsAvailable.MediaInfo) fmt.Printf("MediaInfo support: %t\n", mExtDepsAvailable.MediaInfo)
}
func ReIndexVideos(path []string) {
// filter out those urls which are already existing in db // filter out those urls which are already existing in db
nonExisting := filterExisting(path) nonExisting := filterExisting(path)
fmt.Printf("There are %d videos not existing in db.\n", len(*nonExisting)) fmt.Printf("There are %d videos not existing in db.\n", len(*nonExisting))
for _, s := range *nonExisting { for _, s := range *nonExisting {
processVideo(s) ProcessVideo(s)
} }
AppendMessage("reindex finished successfully!") AppendMessage("reindex finished successfully!")
@ -92,8 +95,8 @@ func filterExisting(paths []string) *[]string {
return &resultarr return &resultarr
} }
func processVideo(fileNameOrig string) { func ProcessVideo(fileNameOrig string) {
fmt.Printf("Processing %s video-", fileNameOrig) fmt.Printf("Processing %s video\n", fileNameOrig)
// match the file extension // match the file extension
r := regexp.MustCompile(`\.[a-zA-Z0-9]+$`) r := regexp.MustCompile(`\.[a-zA-Z0-9]+$`)
@ -120,8 +123,10 @@ func addVideo(videoName string, fileName string, year int) {
Width: 0, Width: 0,
} }
vidFolder := config.GetConfig().General.ReindexPrefix + mSettings.VideoPath
if mExtDepsAvailable.FFMpeg { if mExtDepsAvailable.FFMpeg {
ppic, err = parseFFmpegPic(mSettings.VideoPath + fileName) ppic, err = parseFFmpegPic(vidFolder + fileName)
if err != nil { if err != nil {
fmt.Printf("FFmpeg error occured: %s\n", err.Error()) fmt.Printf("FFmpeg error occured: %s\n", err.Error())
} else { } else {
@ -130,7 +135,7 @@ func addVideo(videoName string, fileName string, year int) {
} }
if mExtDepsAvailable.MediaInfo { if mExtDepsAvailable.MediaInfo {
atr := getVideoAttributes(mSettings.VideoPath + fileName) atr := getVideoAttributes(vidFolder + fileName)
if atr != nil { if atr != nil {
vidAtr = atr vidAtr = atr
} }

View File

@ -3,6 +3,7 @@ package videoparser
import ( import (
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"openmediacenter/apiGo/config"
"openmediacenter/apiGo/database" "openmediacenter/apiGo/database"
"os" "os"
"path/filepath" "path/filepath"
@ -19,20 +20,21 @@ func StartReindex() bool {
SendEvent("start") SendEvent("start")
AppendMessage("starting reindex..") AppendMessage("starting reindex..")
mSettings, PathPrefix, _ := database.GetSettings() mSettings, _, _ := database.GetSettings()
// add the path prefix to videopath // add the path prefix to videopath
mSettings.VideoPath = PathPrefix + mSettings.VideoPath vidFolder := config.GetConfig().General.ReindexPrefix + mSettings.VideoPath
// check if path even exists // check if path even exists
if _, err := os.Stat(mSettings.VideoPath); os.IsNotExist(err) { if _, err := os.Stat(vidFolder); os.IsNotExist(err) {
fmt.Println("Reindex path doesn't exist!") fmt.Println("Reindex path doesn't exist!")
AppendMessage(fmt.Sprintf("Reindex path doesn't exist! :%s", mSettings.VideoPath)) AppendMessage(fmt.Sprintf("Reindex path doesn't exist! :%s", vidFolder))
SendEvent("stop") SendEvent("stop")
return false return false
} }
var files []string var files []string
err := filepath.Walk(mSettings.VideoPath, func(path string, info os.FileInfo, err error) error { err := filepath.Walk(vidFolder, func(path string, info os.FileInfo, err error) error {
if err != nil { if err != nil {
fmt.Println(err.Error()) fmt.Println(err.Error())
return err return err
@ -49,7 +51,8 @@ func StartReindex() bool {
} }
// start reindex process // start reindex process
AppendMessage("Starting Reindexing!") AppendMessage("Starting Reindexing!")
go ReIndexVideos(files, mSettings) InitDeps(&mSettings)
go ReIndexVideos(files)
return true return true
} }

View File

@ -13,7 +13,8 @@ server {
try_files $uri /index.html; try_files $uri /index.html;
} }
location ~* ^/(api/|token) { location ~* ^/(api) {
client_max_body_size 10G;
proxy_pass http://127.0.0.1:8081; proxy_pass http://127.0.0.1:8081;
} }
location /subscribe { location /subscribe {

View File

@ -0,0 +1,30 @@
.dropArea {
border: 2px dashed #ccc;
border-radius: 20px;
width: 480px;
font-family: sans-serif;
padding: 20px;
}
.dropArea:hover {
cursor: pointer;
}
.dropArea.highlight {
border-color: purple;
}
.myForm {
margin-bottom: 10px;
}
.progresswrapper {
width: 100%;
height: 5px;
margin-top: 3px;
}
.finished {
margin-top: 10px;
text-align: center;
}

View File

@ -0,0 +1,108 @@
import style from './DropZone.module.css';
import React, {useState} from 'react';
import {cookie} from '../../utils/context/Cookie';
import GlobalInfos from '../../utils/GlobalInfos';
export const DropZone = (): JSX.Element => {
const [ondrag, setDrag] = useState(0);
const [percent, setpercent] = useState(0.0);
const [finished, setfinished] = useState<string | null>(null);
const theme = GlobalInfos.getThemeStyle();
const uploadFile = (f: FileList): void => {
const xhr = new XMLHttpRequest(); // create XMLHttpRequest
const data = new FormData(); // create formData object
for (let i = 0; i < f.length; i++) {
const file = f.item(i);
if (file) {
data.append('file' + i, file);
}
}
xhr.onload = function (): void {
console.log(this.responseText); // whatever the server returns
const resp = JSON.parse(this.responseText);
if (resp.Message === 'finished all files') {
setfinished('');
} else {
setfinished(resp.Message);
}
setTimeout(() => {
setpercent(0);
setfinished(null);
}, 2000);
};
xhr.upload.onprogress = function (e): void {
console.log(e.loaded / e.total);
setpercent((e.loaded * 100.0) / e.total);
};
xhr.open('post', '/api/video/fileupload'); // open connection
xhr.setRequestHeader('Accept', 'multipart/form-data');
const tkn = cookie.Load();
if (tkn) {
xhr.setRequestHeader('Token', tkn.Token);
}
xhr.send(data); // send data
};
return (
<div
className={style.dropArea + (ondrag > 0 ? ' ' + style.highlight : '') + ' ' + theme.secbackground}
onDragEnter={(e): void => {
e.preventDefault();
e.stopPropagation();
setDrag(ondrag + 1);
}}
onDragLeave={(e): void => {
e.preventDefault();
e.stopPropagation();
setDrag(ondrag - 1);
}}
onDragOver={(e): void => {
e.stopPropagation();
e.preventDefault();
}}
onDrop={(e): void => {
setDrag(0);
e.preventDefault();
e.stopPropagation();
uploadFile(e.dataTransfer.files);
}}
onClick={(): void => {
let input = document.createElement('input');
input.type = 'file';
input.multiple = true;
input.onchange = function (): void {
if (input.files) {
uploadFile(input.files);
}
};
input.click();
}}>
<div className={style.myForm}>
<p>To upload new Videos darg and drop them here or click to select some...</p>
<div className={style.progresswrapper}>
<div style={{width: percent + '%', backgroundColor: 'green', height: 5}} />
</div>
{finished !== null ? (
finished === '' ? (
<div className={style.finished}>Finished uploading</div>
) : (
<div className={style.finished}>Upload failed: {finished}</div>
)
) : (
<></>
)}
</div>
</div>
);
};

View File

@ -11,3 +11,9 @@
padding: 10px; padding: 10px;
width: 40%; width: 40%;
} }
.uploadtext {
font-size: x-large;
margin-top: 30px;
margin-bottom: 10px;
}

View File

@ -2,6 +2,8 @@ import React from 'react';
import style from './MovieSettings.module.css'; import style from './MovieSettings.module.css';
import {APINode, callAPI} from '../../utils/Api'; import {APINode, callAPI} from '../../utils/Api';
import {GeneralSuccess} from '../../types/GeneralTypes'; import {GeneralSuccess} from '../../types/GeneralTypes';
import {DropZone} from '../../elements/DropZone/DropZone';
import GlobalInfos from '../../utils/GlobalInfos';
interface state { interface state {
text: string[]; text: string[];
@ -99,6 +101,8 @@ class MovieSettings extends React.Component<Props, state> {
} }
render(): JSX.Element { render(): JSX.Element {
const theme = GlobalInfos.getThemeStyle();
return ( return (
<> <>
<button <button
@ -123,6 +127,10 @@ class MovieSettings extends React.Component<Props, state> {
</div> </div>
))} ))}
</div> </div>
<div className={theme.textcolor}>
<div className={style.uploadtext}>Video Upload:</div>
<DropZone />
</div>
</> </>
); );
} }