admin: complet API and interface with files checking page
This commit is contained in:
parent
184714aeeb
commit
9a1a64c41c
9 changed files with 166 additions and 19 deletions
|
@ -21,8 +21,8 @@ func init() {
|
||||||
|
|
||||||
router.GET("/api/exercices/:eid/files", apiHandler(exerciceHandler(listExerciceFiles)))
|
router.GET("/api/exercices/:eid/files", apiHandler(exerciceHandler(listExerciceFiles)))
|
||||||
router.POST("/api/exercices/:eid/files", apiHandler(exerciceHandler(createExerciceFile)))
|
router.POST("/api/exercices/:eid/files", apiHandler(exerciceHandler(createExerciceFile)))
|
||||||
router.GET("/api/exercices/:eid/files/:fid", apiHandler(fileHandler(showExerciceFile)))
|
router.GET("/api/exercices/:eid/files/:fid", apiHandler(exerciceFileHandler(showExerciceFile)))
|
||||||
router.DELETE("/api/exercices/:eid/files/:fid", apiHandler(fileHandler(deleteExerciceFile)))
|
router.DELETE("/api/exercices/:eid/files/:fid", apiHandler(exerciceFileHandler(deleteExerciceFile)))
|
||||||
|
|
||||||
router.GET("/api/exercices/:eid/hints", apiHandler(exerciceHandler(listExerciceHints)))
|
router.GET("/api/exercices/:eid/hints", apiHandler(exerciceHandler(listExerciceHints)))
|
||||||
router.POST("/api/exercices/:eid/hints", apiHandler(exerciceHandler(createExerciceHint)))
|
router.POST("/api/exercices/:eid/hints", apiHandler(exerciceHandler(createExerciceHint)))
|
||||||
|
@ -233,6 +233,11 @@ func deleteExerciceQuiz(quiz fic.MCQ, _ fic.Exercice, _ []byte) (interface{}, er
|
||||||
return quiz.Delete()
|
return quiz.Delete()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type uploadedFile struct {
|
||||||
|
URI string
|
||||||
|
Digest string
|
||||||
|
}
|
||||||
|
|
||||||
func createExerciceFile(exercice fic.Exercice, body []byte) (interface{}, error) {
|
func createExerciceFile(exercice fic.Exercice, body []byte) (interface{}, error) {
|
||||||
var uf uploadedFile
|
var uf uploadedFile
|
||||||
if err := json.Unmarshal(body, &uf); err != nil {
|
if err := json.Unmarshal(body, &uf); err != nil {
|
||||||
|
|
|
@ -1,8 +1,50 @@
|
||||||
package api
|
package api
|
||||||
|
|
||||||
import ()
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
|
||||||
type uploadedFile struct {
|
"srs.epita.fr/fic-server/libfic"
|
||||||
URI string
|
|
||||||
Digest string
|
"github.com/julienschmidt/httprouter"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
router.GET("/api/files/", apiHandler(listFiles))
|
||||||
|
router.DELETE("/api/files/", apiHandler(clearFiles))
|
||||||
|
|
||||||
|
router.GET("/api/files/:fileid", apiHandler(fileHandler(showFile)))
|
||||||
|
router.PUT("/api/files/:fileid", apiHandler(fileHandler(updateFile)))
|
||||||
|
router.DELETE("/api/files/:fileid", apiHandler(fileHandler(deleteFile)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func listFiles(_ httprouter.Params, body []byte) (interface{}, error) {
|
||||||
|
// List all files
|
||||||
|
return fic.GetFiles()
|
||||||
|
}
|
||||||
|
|
||||||
|
func clearFiles(_ httprouter.Params, _ []byte) (interface{}, error) {
|
||||||
|
return fic.ClearFiles()
|
||||||
|
}
|
||||||
|
|
||||||
|
func showFile(file fic.EFile, _ []byte) (interface{}, error) {
|
||||||
|
return file, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateFile(file fic.EFile, body []byte) (interface{}, error) {
|
||||||
|
var uf fic.EFile
|
||||||
|
if err := json.Unmarshal(body, &uf); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
uf.Id = file.Id
|
||||||
|
|
||||||
|
if _, err := uf.Update(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else {
|
||||||
|
return uf, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func deleteFile(file fic.EFile, _ []byte) (interface{}, error) {
|
||||||
|
return file.Delete()
|
||||||
}
|
}
|
||||||
|
|
|
@ -213,7 +213,7 @@ func quizHandler(f func(fic.MCQ,fic.Exercice,[]byte) (interface{}, error)) func
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func fileHandler(f func(fic.EFile,[]byte) (interface{}, error)) func (httprouter.Params,[]byte) (interface{}, error) {
|
func exerciceFileHandler(f func(fic.EFile,[]byte) (interface{}, error)) func (httprouter.Params,[]byte) (interface{}, error) {
|
||||||
return func (ps httprouter.Params, body []byte) (interface{}, error) {
|
return func (ps httprouter.Params, body []byte) (interface{}, error) {
|
||||||
var exercice fic.Exercice
|
var exercice fic.Exercice
|
||||||
exerciceHandler(func (ex fic.Exercice, _[]byte) (interface{}, error) {
|
exerciceHandler(func (ex fic.Exercice, _[]byte) (interface{}, error) {
|
||||||
|
@ -248,6 +248,18 @@ func eventHandler(f func(fic.Event,[]byte) (interface{}, error)) func (httproute
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func fileHandler(f func(fic.EFile,[]byte) (interface{}, error)) func (httprouter.Params,[]byte) (interface{}, error) {
|
||||||
|
return func (ps httprouter.Params, body []byte) (interface{}, error) {
|
||||||
|
if fileid, err := strconv.Atoi(string(ps.ByName("fileid"))); err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if file, err := fic.GetFile(fileid); err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else {
|
||||||
|
return f(file, body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func notFound(ps httprouter.Params, _ []byte) (interface{}, error) {
|
func notFound(ps httprouter.Params, _ []byte) (interface{}, error) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,6 +33,7 @@ const indextpl = `<!DOCTYPE html>
|
||||||
<li class="nav-item"><a class="nav-link" href="{{.urlbase}}teams">Équipes</a></li>
|
<li class="nav-item"><a class="nav-link" href="{{.urlbase}}teams">Équipes</a></li>
|
||||||
<li class="nav-item"><a class="nav-link" href="{{.urlbase}}themes">Thèmes</a></li>
|
<li class="nav-item"><a class="nav-link" href="{{.urlbase}}themes">Thèmes</a></li>
|
||||||
<li class="nav-item"><a class="nav-link" href="{{.urlbase}}exercices">Exercices</a></li>
|
<li class="nav-item"><a class="nav-link" href="{{.urlbase}}exercices">Exercices</a></li>
|
||||||
|
<li class="nav-item"><a class="nav-link" href="{{.urlbase}}files">Fichiers</a></li>
|
||||||
<li class="nav-item"><a class="nav-link" href="{{.urlbase}}public/0">Public</a></li>
|
<li class="nav-item"><a class="nav-link" href="{{.urlbase}}public/0">Public</a></li>
|
||||||
<li class="nav-item"><a class="nav-link" href="{{.urlbase}}events">Événements</a></li>
|
<li class="nav-item"><a class="nav-link" href="{{.urlbase}}events">Événements</a></li>
|
||||||
<li class="nav-item"><a class="nav-link" href="{{.urlbase}}settings">Paramètres</a></li>
|
<li class="nav-item"><a class="nav-link" href="{{.urlbase}}settings">Paramètres</a></li>
|
||||||
|
|
|
@ -19,6 +19,9 @@ func init() {
|
||||||
api.Router().GET("/events/*_", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
|
api.Router().GET("/events/*_", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
|
||||||
http.ServeFile(w, r, path.Join(StaticDir, "index.html"))
|
http.ServeFile(w, r, path.Join(StaticDir, "index.html"))
|
||||||
})
|
})
|
||||||
|
api.Router().GET("/files/*_", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
|
||||||
|
http.ServeFile(w, r, path.Join(StaticDir, "index.html"))
|
||||||
|
})
|
||||||
api.Router().GET("/public/*_", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
|
api.Router().GET("/public/*_", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
|
||||||
http.ServeFile(w, r, path.Join(StaticDir, "index.html"))
|
http.ServeFile(w, r, path.Join(StaticDir, "index.html"))
|
||||||
})
|
})
|
||||||
|
|
|
@ -31,6 +31,7 @@
|
||||||
<li class="nav-item"><a class="nav-link" href="/teams">Équipes</a></li>
|
<li class="nav-item"><a class="nav-link" href="/teams">Équipes</a></li>
|
||||||
<li class="nav-item"><a class="nav-link" href="/themes">Thèmes</a></li>
|
<li class="nav-item"><a class="nav-link" href="/themes">Thèmes</a></li>
|
||||||
<li class="nav-item"><a class="nav-link" href="/exercices">Exercices</a></li>
|
<li class="nav-item"><a class="nav-link" href="/exercices">Exercices</a></li>
|
||||||
|
<li class="nav-item"><a class="nav-link" href="/files">Fichiers</a></li>
|
||||||
<li class="nav-item"><a class="nav-link" href="/public/0">Public</a></li>
|
<li class="nav-item"><a class="nav-link" href="/public/0">Public</a></li>
|
||||||
<li class="nav-item"><a class="nav-link" href="/events">Événements</a></li>
|
<li class="nav-item"><a class="nav-link" href="/events">Événements</a></li>
|
||||||
<li class="nav-item"><a class="nav-link" href="/settings">Paramètres</a></li>
|
<li class="nav-item"><a class="nav-link" href="/settings">Paramètres</a></li>
|
||||||
|
|
|
@ -49,6 +49,10 @@ angular.module("FICApp", ["ngRoute", "ngResource", "ngSanitize"])
|
||||||
controller: "PublicController",
|
controller: "PublicController",
|
||||||
templateUrl: "views/public.html"
|
templateUrl: "views/public.html"
|
||||||
})
|
})
|
||||||
|
.when("/files", {
|
||||||
|
controller: "FilesListController",
|
||||||
|
templateUrl: "views/file-list.html"
|
||||||
|
})
|
||||||
.when("/events", {
|
.when("/events", {
|
||||||
controller: "EventsListController",
|
controller: "EventsListController",
|
||||||
templateUrl: "views/event-list.html"
|
templateUrl: "views/event-list.html"
|
||||||
|
@ -72,6 +76,9 @@ angular.module("FICApp")
|
||||||
'update': {method: 'PUT'},
|
'update': {method: 'PUT'},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
.factory("File", function($resource) {
|
||||||
|
return $resource("/api/files/:fileId", { fileId: '@id' })
|
||||||
|
})
|
||||||
.factory("ROSettings", function($resource) {
|
.factory("ROSettings", function($resource) {
|
||||||
return $resource("/api/settings-ro.json")
|
return $resource("/api/settings-ro.json")
|
||||||
})
|
})
|
||||||
|
@ -525,6 +532,37 @@ angular.module("FICApp")
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
|
|
||||||
|
.controller("FilesListController", function($scope, File, $location, $http, $rootScope) {
|
||||||
|
$scope.files = File.query();
|
||||||
|
$scope.fields = ["id", "path", "name", "checksum", "size"];
|
||||||
|
|
||||||
|
$scope.clearFiles = function(id) {
|
||||||
|
File.delete(function() {
|
||||||
|
$scope.files = [];
|
||||||
|
});
|
||||||
|
};
|
||||||
|
$scope.checksum = function(f) {
|
||||||
|
$http({
|
||||||
|
url: "/api/files/" + f.id + "/checksum",
|
||||||
|
method: "GET"
|
||||||
|
}).then(function(response) {
|
||||||
|
if (response.data)
|
||||||
|
$rootScope.newBox('danger', response.data);
|
||||||
|
}, function(response) {
|
||||||
|
$scope.inSync = false;
|
||||||
|
$rootScope.newBox('danger', 'An error occurs when checking files:', response.data);
|
||||||
|
})
|
||||||
|
};
|
||||||
|
$scope.checksumAll = function() {
|
||||||
|
angular.forEach($scope.files, function(file) {
|
||||||
|
$scope.checksum(file);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
$scope.show = function(f) {
|
||||||
|
$location.url("/exercices/" + f.idExercice);
|
||||||
|
};
|
||||||
|
})
|
||||||
|
|
||||||
.controller("EventsListController", function($scope, Event, $location) {
|
.controller("EventsListController", function($scope, Event, $location) {
|
||||||
$scope.events = Event.query();
|
$scope.events = Event.query();
|
||||||
$scope.fields = ["id", "kind", "txt", "time"];
|
$scope.fields = ["id", "kind", "txt", "time"];
|
||||||
|
|
22
admin/static/views/file-list.html
Normal file
22
admin/static/views/file-list.html
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
<h2>
|
||||||
|
Fichiers
|
||||||
|
<button ng-click="checksumAll()" class="float-right btn btn-sm btn-secondary mr-sm-2"><span class="glyphicon glyphicon-flash" aria-hidden="true"></span> Vérifier les fichiers</button>
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<p><input type="search" class="form-control" placeholder="Search" ng-model="query"></p>
|
||||||
|
<table class="table table-hover table-bordered table-striped table-sm">
|
||||||
|
<thead class="thead-dark">
|
||||||
|
<tr>
|
||||||
|
<th ng-repeat="field in fields">
|
||||||
|
{{ field }}
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr ng-repeat="file in files | filter: query" ng-click="show(file)">
|
||||||
|
<td ng-repeat="field in fields">
|
||||||
|
{{ file[field] }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
|
@ -49,6 +49,11 @@ func GetFiles() ([]EFile, error) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetFile(id int) (f EFile, err error) {
|
||||||
|
err = DBQueryRow("SELECT id_file, origin, path, name, cksum, size FROM exercice_files WHERE id_file = ?", id).Scan(&f.Id, &f.origin, &f.Path, &f.Name, &f.Checksum, &f.Size)
|
||||||
|
return f, err
|
||||||
|
}
|
||||||
|
|
||||||
func GetFileByPath(path string) (EFile, error) {
|
func GetFileByPath(path string) (EFile, error) {
|
||||||
path = strings.TrimPrefix(path, FilesDir)
|
path = strings.TrimPrefix(path, FilesDir)
|
||||||
|
|
||||||
|
@ -94,13 +99,13 @@ func (e Exercice) GetFileByPath(path string) (EFile, error) {
|
||||||
return f, nil
|
return f, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e Exercice) ImportFile(filePath string, origin string, digest []byte) (interface{}, error) {
|
func checkFileHash(filePath string, digest []byte) ([]byte, int64, error) {
|
||||||
if digest == nil && !OptionalDigest {
|
if digest == nil {
|
||||||
return EFile{}, errors.New("No digest given.")
|
return []byte{}, 0, errors.New("No digest given.")
|
||||||
} else if fi, err := os.Stat(filePath); err != nil {
|
} else if fi, err := os.Stat(filePath); err != nil {
|
||||||
return EFile{}, err
|
return []byte{}, fi.Size(), err
|
||||||
} else if fd, err := os.Open(filePath); err != nil {
|
} else if fd, err := os.Open(filePath); err != nil {
|
||||||
return EFile{}, err
|
return []byte{}, fi.Size(), err
|
||||||
} else {
|
} else {
|
||||||
defer fd.Close()
|
defer fd.Close()
|
||||||
|
|
||||||
|
@ -110,41 +115,49 @@ func (e Exercice) ImportFile(filePath string, origin string, digest []byte) (int
|
||||||
|
|
||||||
w := io.MultiWriter(hash160, hash512)
|
w := io.MultiWriter(hash160, hash512)
|
||||||
if _, err := io.Copy(w, reader); err != nil {
|
if _, err := io.Copy(w, reader); err != nil {
|
||||||
return EFile{}, err
|
return []byte{}, fi.Size(), err
|
||||||
}
|
}
|
||||||
result160 := hash160.Sum(nil)
|
result160 := hash160.Sum(nil)
|
||||||
result512 := hash512.Sum(nil)
|
result512 := hash512.Sum(nil)
|
||||||
|
|
||||||
if len(digest) != len(result512) {
|
if len(digest) != len(result512) {
|
||||||
if len(digest) != len(result160) {
|
if len(digest) != len(result160) {
|
||||||
return EFile{}, errors.New("Digests doesn't match: calculated: sha1:" + hex.EncodeToString(result160) + " & blake2b:" + hex.EncodeToString(result512) + " vs. given: " + hex.EncodeToString(digest))
|
return []byte{}, fi.Size(), errors.New("Digests doesn't match: calculated: sha1:" + hex.EncodeToString(result160) + " & blake2b:" + hex.EncodeToString(result512) + " vs. given: " + hex.EncodeToString(digest))
|
||||||
} else if StrongDigest {
|
} else if StrongDigest {
|
||||||
return EFile{}, errors.New("Invalid digests: SHA-1 checksums are no more accepted. Calculated sha1:" + hex.EncodeToString(result160) + " & blake2b:" + hex.EncodeToString(result512) + " vs. given: " + hex.EncodeToString(digest))
|
return []byte{}, fi.Size(), errors.New("Invalid digests: SHA-1 checksums are no more accepted. Calculated sha1:" + hex.EncodeToString(result160) + " & blake2b:" + hex.EncodeToString(result512) + " vs. given: " + hex.EncodeToString(digest))
|
||||||
}
|
}
|
||||||
|
|
||||||
for k := range result160 {
|
for k := range result160 {
|
||||||
if result160[k] != digest[k] {
|
if result160[k] != digest[k] {
|
||||||
return EFile{}, errors.New("Digests doesn't match: calculated: sha1:" + hex.EncodeToString(result160) + " & blake2b:" + hex.EncodeToString(result512) + " vs. given: " + hex.EncodeToString(digest))
|
return []byte{}, fi.Size(), errors.New("Digests doesn't match: calculated: sha1:" + hex.EncodeToString(result160) + " & blake2b:" + hex.EncodeToString(result512) + " vs. given: " + hex.EncodeToString(digest))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
for k := range result512 {
|
for k := range result512 {
|
||||||
if result512[k] != digest[k] {
|
if result512[k] != digest[k] {
|
||||||
return EFile{}, errors.New("Digests doesn't match: calculated: " + hex.EncodeToString(result512) + " vs. given: " + hex.EncodeToString(digest))
|
return []byte{}, fi.Size(), errors.New("Digests doesn't match: calculated: " + hex.EncodeToString(result512) + " vs. given: " + hex.EncodeToString(digest))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return result512, fi.Size(), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e Exercice) ImportFile(filePath string, origin string, digest []byte) (interface{}, error) {
|
||||||
|
if result512, size, err := checkFileHash(filePath, digest); !OptionalDigest && err != nil {
|
||||||
|
return EFile{}, err
|
||||||
|
} else {
|
||||||
dPath := strings.TrimPrefix(filePath, FilesDir)
|
dPath := strings.TrimPrefix(filePath, FilesDir)
|
||||||
if f, err := e.GetFileByPath(dPath); err != nil {
|
if f, err := e.GetFileByPath(dPath); err != nil {
|
||||||
return e.AddFile(dPath, origin, path.Base(filePath), result512, fi.Size())
|
return e.AddFile(dPath, origin, path.Base(filePath), result512, size)
|
||||||
} else {
|
} else {
|
||||||
// Don't need to update Path and Name, because they are related to dPath
|
// Don't need to update Path and Name, because they are related to dPath
|
||||||
|
|
||||||
f.IdExercice = e.Id
|
f.IdExercice = e.Id
|
||||||
f.origin = origin
|
f.origin = origin
|
||||||
f.Checksum = result512
|
f.Checksum = result512
|
||||||
f.Size = fi.Size()
|
f.Size = size
|
||||||
|
|
||||||
if _, err := f.Update(); err != nil {
|
if _, err := f.Update(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -195,6 +208,16 @@ func (e Exercice) WipeFiles() (int64, error) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ClearFiles() (int64, error) {
|
||||||
|
if res, err := DBExec("DELETE FROM exercice_files"); err != nil {
|
||||||
|
return 0, err
|
||||||
|
} else if nb, err := res.RowsAffected(); err != nil {
|
||||||
|
return 0, err
|
||||||
|
} else {
|
||||||
|
return nb, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (f EFile) GetOrigin() string {
|
func (f EFile) GetOrigin() string {
|
||||||
return f.origin
|
return f.origin
|
||||||
}
|
}
|
||||||
|
|
Reference in a new issue