开发...

This commit is contained in:
kenneth
2023-11-29 09:46:09 +00:00
parent 6b8eafe5f5
commit d07d13151a
31 changed files with 920 additions and 665 deletions

View File

@@ -12,6 +12,7 @@ type Querier interface {
CreateUser(ctx context.Context, arg CreateUserParams) (User, error)
DeleteUser(ctx context.Context, id string) error
GetUser(ctx context.Context, id string) (User, error)
GetUserByEmail(ctx context.Context, email string) (User, error)
GetUserByName(ctx context.Context, username string) (User, error)
ListUsers(ctx context.Context, arg ListUsersParams) ([]User, error)
UpdateUser(ctx context.Context, arg UpdateUserParams) (User, error)

View File

@@ -1,8 +1,8 @@
-- name: CreateUser :one
INSERT INTO users (
username, hashed_password, email
id, username, hashed_password, email
) VALUES (
$1, $2, $3
$1, $2, $3, $4
)
RETURNING *;
@@ -25,6 +25,10 @@ WHERE id = $1 LIMIT 1;
SELECT * FROM users
WHERE username = $1 LIMIT 1;
-- name: GetUserByEmail :one
SELECT * FROM users
WHERE email = $1 LIMIT 1;
-- name: ListUsers :many
SELECT * FROM users
ORDER BY id

View File

@@ -1,7 +1,7 @@
CREATE TABLE "users" (
"id" varchar NOT NULL PRIMARY KEY,
"username" varchar NOT NULL,
"username" varchar NOT NULL UNIQUE,
"hashed_password" varchar NOT NULL,
"email" varchar NOT NULL,
"email" varchar NOT NULL UNIQUE,
"created_at" timestamptz NOT NULL DEFAULT (now())
);

View File

@@ -11,21 +11,27 @@ import (
const createUser = `-- name: CreateUser :one
INSERT INTO users (
username, hashed_password, email
id, username, hashed_password, email
) VALUES (
$1, $2, $3
$1, $2, $3, $4
)
RETURNING id, username, hashed_password, email, created_at
`
type CreateUserParams struct {
ID string `json:"id"`
Username string `json:"username"`
HashedPassword string `json:"hashed_password"`
Email string `json:"email"`
}
func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (User, error) {
row := q.db.QueryRowContext(ctx, createUser, arg.Username, arg.HashedPassword, arg.Email)
row := q.db.QueryRowContext(ctx, createUser,
arg.ID,
arg.Username,
arg.HashedPassword,
arg.Email,
)
var i User
err := row.Scan(
&i.ID,
@@ -65,6 +71,24 @@ func (q *Queries) GetUser(ctx context.Context, id string) (User, error) {
return i, err
}
const getUserByEmail = `-- name: GetUserByEmail :one
SELECT id, username, hashed_password, email, created_at FROM users
WHERE email = $1 LIMIT 1
`
func (q *Queries) GetUserByEmail(ctx context.Context, email string) (User, error) {
row := q.db.QueryRowContext(ctx, getUserByEmail, email)
var i User
err := row.Scan(
&i.ID,
&i.Username,
&i.HashedPassword,
&i.Email,
&i.CreatedAt,
)
return i, err
}
const getUserByName = `-- name: GetUserByName :one
SELECT id, username, hashed_password, email, created_at FROM users
WHERE username = $1 LIMIT 1

37
internal/handlers/base.go Normal file
View File

@@ -0,0 +1,37 @@
package handlers
import (
"net/http"
"github.com/rs/xid"
"github.com/zhang2092/mediahls/internal/pkg/cookie"
)
const (
AuthorizeCookie = "authorize"
ContextUser CtxTypeUser = "context_user"
)
type CtxTypeUser string
type authorize struct {
AuthID string `json:"auth_id"`
AuthName string `json:"auth_name"`
}
func genId() string {
id := xid.New()
return id.String()
}
func (server *Server) isRedirect(w http.ResponseWriter, r *http.Request) {
_, err := server.withCookie(r)
if err != nil {
// 1. 删除cookie
cookie.DeleteCookie(w, cookie.AuthorizeName)
return
}
// cookie 校验成功
http.Redirect(w, r, "/", http.StatusFound)
}

View File

@@ -1,20 +1,24 @@
package handlers
import (
"html/template"
"net/http"
)
func (server *Server) home(w http.ResponseWriter, r *http.Request) {
t, err := template.ParseFiles("web/templates/home.html.tmpl")
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
err = t.Execute(w, nil)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
type pageData struct {
AuthID string
AuthName string
}
func (server *Server) home(w http.ResponseWriter, r *http.Request) {
pd := pageData{}
auth, err := server.withCookie(r)
if err == nil {
pd.AuthID = auth.AuthID
pd.AuthName = auth.AuthName
}
renderHome(w, pd)
}
func renderHome(w http.ResponseWriter, data any) {
render(w, data, "web/templates/home.html.tmpl", "web/templates/base/header.html.tmpl", "web/templates/base/footer.html.tmpl")
}

View File

@@ -0,0 +1,65 @@
package handlers
import (
"context"
"encoding/json"
"log"
"net/http"
"github.com/zhang2092/mediahls/internal/pkg/convert"
)
func (server *Server) authorizeMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
u, err := server.withCookie(r)
if err != nil {
http.Redirect(w, r, "/login", http.StatusFound)
return
}
b, err := json.Marshal(u)
if err != nil {
log.Printf("json marshal authorize user: %v", err)
http.Redirect(w, r, "/login", http.StatusFound)
return
}
ctx := r.Context()
ctx = context.WithValue(ctx, ContextUser, b)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
func (server *Server) withCookie(r *http.Request) (*authorize, error) {
cookie, err := r.Cookie(AuthorizeCookie)
if err != nil {
log.Printf("get cookie: %v", err)
return nil, err
}
u := &authorize{}
err = server.secureCookie.Decode(AuthorizeCookie, cookie.Value, u)
if err != nil {
log.Printf("secure decode cookie: %v", err)
return nil, err
}
return u, nil
}
func withUser(ctx context.Context) *authorize {
var result authorize
ctxValue, err := convert.ToByteE(ctx.Value(ContextUser))
log.Printf("ctx: %s", ctxValue)
if err != nil {
log.Printf("1: %v", err)
return nil
}
err = json.Unmarshal(ctxValue, &result)
if err != nil {
log.Printf("2: %v", err)
return nil
}
return &result
}

View File

@@ -0,0 +1,23 @@
package handlers
import (
"html/template"
"log"
"net/http"
)
func render(w http.ResponseWriter, data any, tmpls ...string) {
t, err := template.ParseFiles(tmpls...)
if err != nil {
log.Printf("template parse: %v", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
err = t.Execute(w, data)
if err != nil {
log.Printf("template execute: %v", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
}

View File

@@ -13,6 +13,7 @@ import (
"time"
"github.com/gorilla/mux"
"github.com/gorilla/securecookie"
"github.com/zhang2092/mediahls/internal/db"
"github.com/zhang2092/mediahls/internal/pkg/config"
"github.com/zhang2092/mediahls/internal/pkg/logger"
@@ -20,8 +21,9 @@ import (
)
type Server struct {
conf *config.Config
router *mux.Router
conf *config.Config
router *mux.Router
secureCookie *securecookie.SecureCookie
store db.Store
tokenMaker token.Maker
@@ -33,10 +35,16 @@ func NewServer(conf *config.Config, store db.Store) (*Server, error) {
return nil, fmt.Errorf("cannot create token maker: %w", err)
}
hashKey := securecookie.GenerateRandomKey(32)
blockKey := securecookie.GenerateRandomKey(32)
secureCookie := securecookie.New(hashKey, blockKey)
secureCookie.MaxAge(7200)
server := &Server{
conf: conf,
store: store,
tokenMaker: tokenMaker,
conf: conf,
secureCookie: secureCookie,
store: store,
tokenMaker: tokenMaker,
}
server.setupRouter()
@@ -45,18 +53,23 @@ func NewServer(conf *config.Config, store db.Store) (*Server, error) {
func (server *Server) setupRouter() {
router := mux.NewRouter()
router.Use(mux.CORSMethodMiddleware(router))
router.PathPrefix("/statics/").Handler(http.StripPrefix("/statics/", http.FileServer(http.Dir("web/statics"))))
router.HandleFunc("/register", server.registerView).Methods(http.MethodGet)
router.HandleFunc("/register", server.register).Methods(http.MethodPost)
router.HandleFunc("/login", server.loginView).Methods(http.MethodGet)
router.HandleFunc("/login", server.login).Methods(http.MethodPost)
router.HandleFunc("/logout", server.logout).Methods(http.MethodGet)
router.HandleFunc("/", server.home).Methods(http.MethodGet)
router.HandleFunc("/play/{xid}", server.play).Methods(http.MethodGet)
router.HandleFunc("/media/{xid}/stream/", server.stream).Methods(http.MethodGet)
router.HandleFunc("/media/{xid}/stream/{segName:index[0-9]+.ts}", server.stream).Methods(http.MethodGet)
router.HandleFunc("/upload", server.upload).Methods(http.MethodGet, http.MethodPost)
subRouter := router.PathPrefix("/").Subrouter()
subRouter.Use(server.authorizeMiddleware)
subRouter.HandleFunc("/upload", server.uploadView).Methods(http.MethodGet)
subRouter.HandleFunc("/upload", server.upload).Methods(http.MethodPost)
server.router = router
}

View File

@@ -1,7 +1,6 @@
package handlers
import (
"html/template"
"io"
"log"
"net/http"
@@ -14,89 +13,83 @@ import (
"github.com/zhang2092/mediahls/internal/pkg/fileutil"
)
func (server *Server) uploadView(w http.ResponseWriter, r *http.Request) {
user := withUser(r.Context())
log.Printf("%v", user)
renderUpload(w, nil)
}
func renderUpload(w http.ResponseWriter, data any) {
render(w, data, "web/templates/me/upload.html.tmpl", "web/templates/base/header.html.tmpl", "web/templates/base/footer.html.tmpl")
}
func (server *Server) upload(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodPost {
defer r.Body.Close()
defer r.Body.Close()
file, fileHeader, err := r.FormFile("file")
if err != nil {
w.WriteHeader(http.StatusBadRequest)
_, err = w.Write([]byte(err.Error()))
if err != nil {
log.Printf("%v", err)
}
return
}
defer file.Close()
buff := make([]byte, 512)
_, err = file.Read(buff)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// filetype := http.DetectContentType(buff)
// if filetype != "image/jpeg" && filetype != "image/png" {
// http.Error(w, "The provided file format is not allowed. Please upload a JPEG or PNG image", http.StatusBadRequest)
// return
// }
_, err = file.Seek(0, io.SeekStart)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
dir := path.Join("upload", time.Now().Format("20060102"))
exist, _ := fileutil.PathExists(dir)
if !exist {
err := os.MkdirAll(dir, os.ModePerm)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
filename, err := nanoId.Nanoid()
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
filePath := path.Join("", dir, filename+filepath.Ext(fileHeader.Filename))
f, err := os.Create(filePath)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
defer f.Close()
_, err = io.Copy(f, file)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
w.WriteHeader(http.StatusOK)
_, err = w.Write([]byte(filePath))
file, fileHeader, err := r.FormFile("file")
if err != nil {
w.WriteHeader(http.StatusBadRequest)
_, err = w.Write([]byte(err.Error()))
if err != nil {
log.Printf("%v", err)
}
return
}
defer file.Close()
buff := make([]byte, 512)
_, err = file.Read(buff)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
t, err := template.ParseFiles("web/templates/upload.html.tmpl")
// filetype := http.DetectContentType(buff)
// if filetype != "image/jpeg" && filetype != "image/png" {
// http.Error(w, "The provided file format is not allowed. Please upload a JPEG or PNG image", http.StatusBadRequest)
// return
// }
_, err = file.Seek(0, io.SeekStart)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
err = t.Execute(w, nil)
dir := path.Join("upload", time.Now().Format("20060102"))
exist, _ := fileutil.PathExists(dir)
if !exist {
err := os.MkdirAll(dir, os.ModePerm)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
filename, err := nanoId.Nanoid()
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
filePath := path.Join("", dir, filename+filepath.Ext(fileHeader.Filename))
f, err := os.Create(filePath)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
defer f.Close()
_, err = io.Copy(f, file)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
w.WriteHeader(http.StatusOK)
_, err = w.Write([]byte(filePath))
if err != nil {
log.Printf("%v", err)
}
}

View File

@@ -2,45 +2,72 @@ package handlers
import (
"database/sql"
"html/template"
"log"
"net/http"
"time"
"github.com/zhang2092/mediahls/internal/db"
"github.com/zhang2092/mediahls/internal/pkg/cookie"
pwd "github.com/zhang2092/mediahls/internal/pkg/password"
)
func (server *Server) registerView(w http.ResponseWriter, r *http.Request) {
t, err := template.ParseFiles("web/templates/user/register.html.tmpl", "web/templates/base/header.html.tmpl", "web/templates/base/footer.html.tmpl")
if err != nil {
log.Printf("%v", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
err = t.Execute(w, nil)
if err != nil {
log.Printf("%v", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
// 是否已经登录
server.isRedirect(w, r)
renderRegister(w, nil)
}
type userResponse struct {
Username string `json:"username"`
FullName string `json:"full_name"`
Email string `json:"email"`
PasswordChangedAt time.Time `json:"password_changed_at"`
CreatedAt time.Time `json:"created_at"`
func renderRegister(w http.ResponseWriter, data any) {
render(w, data, "web/templates/user/register.html.tmpl", "web/templates/base/header.html.tmpl", "web/templates/base/footer.html.tmpl")
}
func newUserResponse(user db.User) userResponse {
return userResponse{
Username: user.Username,
Email: user.Email,
CreatedAt: user.CreatedAt,
// type userResponse struct {
// Username string `json:"username"`
// FullName string `json:"full_name"`
// Email string `json:"email"`
// PasswordChangedAt time.Time `json:"password_changed_at"`
// CreatedAt time.Time `json:"created_at"`
// }
// func newUserResponse(user db.User) userResponse {
// return userResponse{
// Username: user.Username,
// Email: user.Email,
// CreatedAt: user.CreatedAt,
// }
// }
func viladatorRegister(email, username, password string) (*respErrs, bool) {
ok := true
errs := &respErrs{
Email: email,
Username: username,
Password: password,
}
if !ValidateRxEmail(email) {
errs.EmailErr = "请填写正确的邮箱地址"
ok = false
}
if !ValidateRxUsername(username) {
errs.UsernameErr = "名称(6-20,字母,数字)"
ok = false
}
if !ValidatePassword(password) {
errs.PasswordErr = "密码(8-20位)"
ok = false
}
return errs, ok
}
type respErrs struct {
Summary string
Email string
Username string
Password string
EmailErr string
UsernameErr string
PasswordErr string
}
func (server *Server) register(w http.ResponseWriter, r *http.Request) {
@@ -51,9 +78,14 @@ func (server *Server) register(w http.ResponseWriter, r *http.Request) {
return
}
username := r.PostFormValue("username")
email := r.PostFormValue("email")
username := r.PostFormValue("username")
password := r.PostFormValue("password")
errs, ok := viladatorRegister(email, username, password)
if !ok {
renderRegister(w, errs)
return
}
hashedPassword, err := pwd.BcryptHashPassword(password)
if err != nil {
@@ -62,88 +94,116 @@ func (server *Server) register(w http.ResponseWriter, r *http.Request) {
}
arg := db.CreateUserParams{
ID: genId(),
Username: username,
HashedPassword: hashedPassword,
Email: email,
}
user, err := server.store.CreateUser(r.Context(), arg)
_, err = server.store.CreateUser(r.Context(), arg)
if err != nil {
if server.store.IsUniqueViolation(err) {
http.Error(w, "数据已经存在", http.StatusInternalServerError)
errs.Summary = "邮箱或名称已经存在"
renderRegister(w, errs)
return
}
http.Error(w, err.Error(), http.StatusInternalServerError)
errs.Summary = "请求网络错误,请刷新重试"
renderRegister(w, errs)
return
}
rsp := newUserResponse(user)
Respond(w, "ok", rsp, http.StatusOK)
http.Redirect(w, r, "/login", http.StatusFound)
// rsp := newUserResponse(user)
// Respond(w, "ok", rsp, http.StatusOK)
}
func (server *Server) loginView(w http.ResponseWriter, r *http.Request) {
t, err := template.ParseFiles("web/templates/user/login.html.tmpl", "web/templates/base/header.html.tmpl", "web/templates/base/footer.html.tmpl")
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
err = t.Execute(w, nil)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
// 是否已经登录
server.isRedirect(w, r)
renderLogin(w, nil)
}
type loginUserResponse struct {
AccessToken string `json:"access_token"`
AccessTokenExpiresAt time.Time `json:"access_token_expires_at"`
User userResponse `json:"user"`
func renderLogin(w http.ResponseWriter, data any) {
render(w, data, "web/templates/user/login.html.tmpl", "web/templates/base/header.html.tmpl", "web/templates/base/footer.html.tmpl")
}
// type loginUserResponse struct {
// AccessToken string `json:"access_token"`
// AccessTokenExpiresAt time.Time `json:"access_token_expires_at"`
// User userResponse `json:"user"`
// }
func viladatorLogin(email, password string) (*respErrs, bool) {
ok := true
errs := &respErrs{
Email: email,
Password: password,
}
if !ValidateRxEmail(email) {
errs.EmailErr = "请填写正确的邮箱地址"
ok = false
}
if len(password) == 0 {
errs.PasswordErr = "请填写正确的密码"
ok = false
}
return errs, ok
}
func (server *Server) login(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
if err := r.ParseForm(); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
renderLogin(w, respErrs{Summary: "请求网络错误,请刷新重试"})
return
}
username := r.PostFormValue("username")
email := r.PostFormValue("email")
password := r.PostFormValue("password")
ctx := r.Context()
errs, ok := viladatorLogin(email, password)
if !ok {
renderLogin(w, errs)
return
}
user, err := server.store.GetUserByName(ctx, username)
ctx := r.Context()
user, err := server.store.GetUserByEmail(ctx, email)
if err != nil {
if server.store.IsNoRows(sql.ErrNoRows) {
http.Error(w, "用户不存在", http.StatusInternalServerError)
errs.Summary = "邮箱或密码错误"
renderLogin(w, errs)
return
}
http.Error(w, err.Error(), http.StatusInternalServerError)
errs.Summary = "请求网络错误,请刷新重试"
renderLogin(w, errs)
return
}
err = pwd.BcryptComparePassword(password, user.HashedPassword)
err = pwd.BcryptComparePassword(user.HashedPassword, password)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
errs.Summary = "邮箱或密码错误"
renderLogin(w, errs)
return
}
accessToken, accessPayload, err := server.tokenMaker.CreateToken(
user.ID,
user.Username,
server.conf.AccessTokenDuration,
)
encoded, err := server.secureCookie.Encode(AuthorizeCookie, &authorize{AuthID: user.ID, AuthName: user.Username})
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
errs.Summary = "请求网络错误,请刷新重试(cookie)"
renderLogin(w, errs)
return
}
rsp := loginUserResponse{
AccessToken: accessToken,
AccessTokenExpiresAt: accessPayload.ExpiresAt.Time,
User: newUserResponse(user),
}
Respond(w, "ok", rsp, http.StatusOK)
c := cookie.NewCookie(cookie.AuthorizeName, encoded, time.Now().Add(time.Duration(7200)*time.Second))
http.SetCookie(w, c)
http.Redirect(w, r, "/", http.StatusFound)
}
func (server *Server) logout(w http.ResponseWriter, r *http.Request) {
cookie.DeleteCookie(w, cookie.AuthorizeName)
http.Redirect(w, r, "/login", http.StatusFound)
}

View File

@@ -0,0 +1,38 @@
package handlers
import (
"regexp"
"strings"
)
var (
rxPhone = regexp.MustCompile(`^(13|14|15|16|17|18|19)\d{9}$`)
rxEmail = regexp.MustCompile(`\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*`)
rxUsername = regexp.MustCompile(`^[a-z0-9A-Z]{6,20}$`) // 6到20位字母数字
//rxPassword = regexp.MustCompile(`^(?=.*[a-zA-Z])(?=.*[0-9])[A-Za-z0-9]{8,18}$`) // 最少6位包括至少1个大写字母1个小写字母1个数字1个特殊字符
)
func ValidateRxPhone(phone string) bool {
phone = strings.TrimSpace(phone)
return rxPhone.MatchString(phone)
}
func ValidateRxEmail(email string) bool {
email = strings.TrimSpace(email)
return rxEmail.MatchString(email)
}
func ValidateRxUsername(username string) bool {
username = strings.TrimSpace(username)
return rxUsername.MatchString(username)
}
// func ValidateRxPassword(password string) bool {
// password = strings.TrimSpace(password)
// return rxPassword.MatchString(password)
// }
func ValidatePassword(password string) bool {
password = strings.TrimSpace(password)
return len(password) >= 8 && len(password) <= 20
}

View File

@@ -1,9 +1,30 @@
package handlers
import "net/http"
import (
"fmt"
"net/http"
"github.com/gorilla/mux"
)
type playData struct {
AuthID string
AuthName string
Url string
}
func (server *Server) play(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
xid := vars["xid"]
data := playData{
Url: "/media/" + xid + "/stream/",
}
auth, err := server.withCookie(r)
if err == nil {
data.AuthID = auth.AuthID
data.AuthName = auth.AuthName
}
render(w, data, "web/templates/video/play.html.tmpl", "web/templates/base/header.html.tmpl", "web/templates/base/footer.html.tmpl")
}
/*
@@ -18,3 +39,45 @@ defer video.Close()
http.ServeContent(w, r, "git", time.Now(), video)
*/
func (server *Server) stream(response http.ResponseWriter, request *http.Request) {
vars := mux.Vars(request)
mId := vars["xid"]
fmt.Println(mId)
segName, ok := vars["segName"]
if !ok {
mediaBase := getMediaBase(mId)
m3u8Name := "index.m3u8"
serveHlsM3u8(response, request, mediaBase, m3u8Name)
} else {
mediaBase := getMediaBase(mId)
serveHlsTs(response, request, mediaBase, segName)
}
}
func getMediaBase(mId string) string {
mediaRoot := "media"
return fmt.Sprintf("%s/%s", mediaRoot, mId)
}
func serveHlsM3u8(w http.ResponseWriter, r *http.Request, mediaBase, m3u8Name string) {
fmt.Println("serveHlsM3u8...")
mediaFile := fmt.Sprintf("%s/%s", mediaBase, m3u8Name)
fmt.Println(mediaFile)
http.ServeFile(w, r, mediaFile)
w.Header().Set("Content-Type", "application/x-mpegURL")
}
func serveHlsTs(w http.ResponseWriter, r *http.Request, mediaBase, segName string) {
fmt.Println("serveHlsTs...")
mediaFile := fmt.Sprintf("%s/%s", mediaBase, segName)
fmt.Println(mediaFile)
http.ServeFile(w, r, mediaFile)
w.Header().Set("Content-Type", "video/MP2T")
}

View File

@@ -0,0 +1,99 @@
package convert
import (
"encoding/json"
"fmt"
"html/template"
"reflect"
"strconv"
)
var (
errorType = reflect.TypeOf((*error)(nil)).Elem()
fmtStringerType = reflect.TypeOf((*fmt.Stringer)(nil)).Elem()
)
// Copied from html/template/content.go.
// indirectToStringerOrError returns the value, after dereferencing as many times
// as necessary to reach the base type (or nil) or an implementation of fmt.Stringer
// or error,
func indirectToStringerOrError(a any) any {
if a == nil {
return nil
}
v := reflect.ValueOf(a)
for !v.Type().Implements(fmtStringerType) && !v.Type().Implements(errorType) && v.Kind() == reflect.Pointer && !v.IsNil() {
v = v.Elem()
}
return v.Interface()
}
// ToStringE converts any type to a string type.
func ToStringE(i any) (string, error) {
i = indirectToStringerOrError(i)
switch s := i.(type) {
case string:
return s, nil
case bool:
return strconv.FormatBool(s), nil
case float64:
return strconv.FormatFloat(s, 'f', -1, 64), nil
case float32:
return strconv.FormatFloat(float64(s), 'f', -1, 32), nil
case int:
return strconv.Itoa(s), nil
case int64:
return strconv.FormatInt(s, 10), nil
case int32:
return strconv.Itoa(int(s)), nil
case int16:
return strconv.FormatInt(int64(s), 10), nil
case int8:
return strconv.FormatInt(int64(s), 10), nil
case uint:
return strconv.FormatUint(uint64(s), 10), nil
case uint64:
return strconv.FormatUint(uint64(s), 10), nil
case uint32:
return strconv.FormatUint(uint64(s), 10), nil
case uint16:
return strconv.FormatUint(uint64(s), 10), nil
case uint8:
return strconv.FormatUint(uint64(s), 10), nil
case json.Number:
return s.String(), nil
case []byte:
return string(s), nil
case template.HTML:
return string(s), nil
case template.URL:
return string(s), nil
case template.JS:
return string(s), nil
case template.CSS:
return string(s), nil
case template.HTMLAttr:
return string(s), nil
case nil:
return "", nil
case fmt.Stringer:
return s.String(), nil
case error:
return s.Error(), nil
default:
return "", fmt.Errorf("unable to cast %#v of type %T to string", i, i)
}
}
// ToStringE converts any type to a string type.
func ToByteE(i any) ([]byte, error) {
i = indirectToStringerOrError(i)
switch s := i.(type) {
case []byte:
return s, nil
default:
return nil, fmt.Errorf("unable to cast %#v of type %T to string", i, i)
}
}

View File

@@ -1,24 +1,41 @@
package convert
import (
"log"
"os"
"os/exec"
"strings"
"github.com/zhang2092/mediahls/internal/pkg/fileutil"
)
func ConvertHLS(savePath, filePath string) error {
exist, _ := fileutil.PathExists(savePath)
if !exist {
err := os.MkdirAll(savePath, os.ModePerm)
if err != nil {
return err
}
}
binary, err := exec.LookPath("ffmpeg")
if err != nil {
log.Println("1: ", err)
return err
}
// ffmpeg -i web/statics/git.mp4 -profile:v baseline -level 3.0 -s 1920x1080 -start_number 0 -hls_time 10 -hls_list_size 0 -hls_segment_filename %d.ts -f hls web/statics/git.m3u8
command := "-i " + filePath + " -profile:v baseline -level 3.0 -s 1920x1080 -start_number 0 -hls_time 10 -hls_list_size 0 -f hls " + savePath + "index.m3u8"
log.Println(command)
args := strings.Split(command, " ")
cmd := exec.Command(binary, args...)
_, err = cmd.Output()
if err != nil {
log.Println("2: ", err)
return err
}
return nil
// ffmpeg -i upload/20231129/o6e6qKaMdk0VC1Ys2SHnr.mp4 -profile:v baseline -level 3.0 -s 1920x1080 -start_number 0 -f hls -hls_time 10 -segment_list web/statics/js/index.m3u8 web/statics/mmm/index.m3u8
// ffmpeg -i upload/20231129/o6e6qKaMdk0VC1Ys2SHnr.mp4 -c copy -map 0 -f segment -segment_list web/statics/js/index.m3u8 -segment_time 20 web/statics/js/index_%3d.ts
}

View File

@@ -0,0 +1,41 @@
package cookie
import (
"net/http"
"time"
)
const (
AuthorizeName = "authorize"
)
func NewCookie(name, value string, expired time.Time) *http.Cookie {
return &http.Cookie{
Name: name,
Value: value,
Path: "/",
Secure: false, // true->只能https站点操作
HttpOnly: true, // true->js不能捕获
Expires: expired,
}
}
func SetCookie(w http.ResponseWriter, name, value string, expired time.Time) {
cookie := NewCookie(name, value, expired)
http.SetCookie(w, cookie)
}
func ReadCookie(r *http.Request, name string) (string, error) {
cookie, err := r.Cookie(name)
if err != nil {
return "", err
}
return cookie.Value, nil
}
func DeleteCookie(w http.ResponseWriter, name string) {
cookie := NewCookie(name, "", time.Now().Add(time.Duration(-10)*time.Second))
cookie.MaxAge = -1
http.SetCookie(w, cookie)
}