- Add __bad__ for bad torrents
- Add colors to logs info - Make logs a bit more clearer - Mark torrent has bad instead of deleting them
This commit is contained in:
@@ -45,7 +45,24 @@ func New(prefix string) zerolog.Logger {
|
|||||||
TimeFormat: "2006-01-02 15:04:05",
|
TimeFormat: "2006-01-02 15:04:05",
|
||||||
NoColor: false, // Set to true if you don't want colors
|
NoColor: false, // Set to true if you don't want colors
|
||||||
FormatLevel: func(i interface{}) string {
|
FormatLevel: func(i interface{}) string {
|
||||||
return strings.ToUpper(fmt.Sprintf("| %-6s|", i))
|
var colorCode string
|
||||||
|
switch strings.ToLower(fmt.Sprintf("%s", i)) {
|
||||||
|
case "debug":
|
||||||
|
colorCode = "\033[36m"
|
||||||
|
case "info":
|
||||||
|
colorCode = "\033[32m"
|
||||||
|
case "warn":
|
||||||
|
colorCode = "\033[33m"
|
||||||
|
case "error":
|
||||||
|
colorCode = "\033[31m"
|
||||||
|
case "fatal":
|
||||||
|
colorCode = "\033[35m"
|
||||||
|
case "panic":
|
||||||
|
colorCode = "\033[41m"
|
||||||
|
default:
|
||||||
|
colorCode = "\033[37m" // White
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s| %-6s|\033[0m", colorCode, strings.ToUpper(fmt.Sprintf("%s", i)))
|
||||||
},
|
},
|
||||||
FormatMessage: func(i interface{}) string {
|
FormatMessage: func(i interface{}) string {
|
||||||
return fmt.Sprintf("[%s] %v", prefix, i)
|
return fmt.Sprintf("[%s] %v", prefix, i)
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ type CachedTorrent struct {
|
|||||||
*types.Torrent
|
*types.Torrent
|
||||||
AddedOn time.Time `json:"added_on"`
|
AddedOn time.Time `json:"added_on"`
|
||||||
IsComplete bool `json:"is_complete"`
|
IsComplete bool `json:"is_complete"`
|
||||||
|
Bad bool `json:"bad"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c CachedTorrent) copy() CachedTorrent {
|
func (c CachedTorrent) copy() CachedTorrent {
|
||||||
@@ -285,7 +286,11 @@ func (c *Cache) load() (map[string]CachedTorrent, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *Cache) Sync() error {
|
func (c *Cache) Sync() error {
|
||||||
defer c.logger.Info().Msg("WebDav server sync complete")
|
cfg := config.Get()
|
||||||
|
name := c.client.GetName()
|
||||||
|
addr := cfg.BindAddress + ":" + cfg.Port + cfg.URLBase + "webdav/" + name + "/"
|
||||||
|
|
||||||
|
defer c.logger.Info().Msgf("%s WebDav server running at %s", name, addr)
|
||||||
cachedTorrents, err := c.load()
|
cachedTorrents, err := c.load()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.logger.Error().Err(err).Msg("Failed to load cache")
|
c.logger.Error().Err(err).Msg("Failed to load cache")
|
||||||
@@ -298,7 +303,7 @@ func (c *Cache) Sync() error {
|
|||||||
|
|
||||||
totalTorrents := len(torrents)
|
totalTorrents := len(torrents)
|
||||||
|
|
||||||
c.logger.Info().Msgf("Got %d torrents from %s", totalTorrents, c.client.GetName())
|
c.logger.Info().Msgf("%d torrents found from %s", totalTorrents, c.client.GetName())
|
||||||
|
|
||||||
newTorrents := make([]*types.Torrent, 0)
|
newTorrents := make([]*types.Torrent, 0)
|
||||||
idStore := make(map[string]struct{}, totalTorrents)
|
idStore := make(map[string]struct{}, totalTorrents)
|
||||||
@@ -475,12 +480,6 @@ func (c *Cache) GetCustomFolders() []string {
|
|||||||
return c.customFolders
|
return c.customFolders
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Cache) GetDirectories() []string {
|
|
||||||
dirs := []string{"__all__", "torrents"}
|
|
||||||
dirs = append(dirs, c.customFolders...)
|
|
||||||
return dirs
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Cache) Close() error {
|
func (c *Cache) Close() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -711,9 +710,7 @@ func (c *Cache) deleteTorrent(id string, removeFromDebrid bool) bool {
|
|||||||
t.Files = newFiles
|
t.Files = newFiles
|
||||||
newId = cmp.Or(newId, t.Id)
|
newId = cmp.Or(newId, t.Id)
|
||||||
t.Id = newId
|
t.Id = newId
|
||||||
c.setTorrent(t, func(tor CachedTorrent) {
|
c.setTorrent(t, nil) // This gets called after calling deleteTorrent
|
||||||
c.RefreshListings(false)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package debrid
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/sirrobot01/decypharr/internal/config"
|
|
||||||
"github.com/sirrobot01/decypharr/internal/request"
|
"github.com/sirrobot01/decypharr/internal/request"
|
||||||
"github.com/sirrobot01/decypharr/internal/utils"
|
"github.com/sirrobot01/decypharr/internal/utils"
|
||||||
"github.com/sirrobot01/decypharr/pkg/debrid/types"
|
"github.com/sirrobot01/decypharr/pkg/debrid/types"
|
||||||
@@ -34,6 +33,25 @@ func (r *reInsertRequest) Wait() (*CachedTorrent, error) {
|
|||||||
return r.result, r.err
|
return r.result, r.err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Cache) markAsFailedToReinsert(torrentId string) {
|
||||||
|
currentCount := 0
|
||||||
|
if retryCount, ok := c.failedToReinsert.Load(torrentId); ok {
|
||||||
|
currentCount = retryCount.(int)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.failedToReinsert.Store(torrentId, currentCount+1)
|
||||||
|
|
||||||
|
// Remove the torrent from the directory if it has failed to reinsert, max retries are hardcoded to 5
|
||||||
|
if currentCount > 3 {
|
||||||
|
// Mark torrent as failed
|
||||||
|
if torrent, ok := c.torrents.getByID(torrentId); ok {
|
||||||
|
torrent.Bad = true
|
||||||
|
c.SaveTorrent(torrent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Cache) IsTorrentBroken(t *CachedTorrent, filenames []string) bool {
|
func (c *Cache) IsTorrentBroken(t *CachedTorrent, filenames []string) bool {
|
||||||
// Check torrent files
|
// Check torrent files
|
||||||
|
|
||||||
@@ -85,9 +103,8 @@ func (c *Cache) IsTorrentBroken(t *CachedTorrent, filenames []string) bool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
cfg := config.Get()
|
|
||||||
// Try to reinsert the torrent if it's broken
|
// Try to reinsert the torrent if it's broken
|
||||||
if cfg.Repair.ReInsert && isBroken && t.Torrent != nil {
|
if isBroken && t.Torrent != nil {
|
||||||
// Check if the torrent is already in progress
|
// Check if the torrent is already in progress
|
||||||
if _, err := c.reInsertTorrent(t); err != nil {
|
if _, err := c.reInsertTorrent(t); err != nil {
|
||||||
c.logger.Error().Err(err).Str("torrentId", t.Torrent.Id).Msg("Failed to reinsert torrent")
|
c.logger.Error().Err(err).Str("torrentId", t.Torrent.Id).Msg("Failed to reinsert torrent")
|
||||||
@@ -161,14 +178,14 @@ func (c *Cache) reInsertTorrent(ct *CachedTorrent) (*CachedTorrent, error) {
|
|||||||
var err error
|
var err error
|
||||||
newTorrent, err = c.client.SubmitMagnet(newTorrent)
|
newTorrent, err = c.client.SubmitMagnet(newTorrent)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.failedToReinsert.Store(oldID, struct{}{})
|
c.markAsFailedToReinsert(oldID)
|
||||||
// Remove the old torrent from the cache and debrid service
|
// Remove the old torrent from the cache and debrid service
|
||||||
return ct, fmt.Errorf("failed to submit magnet: %w", err)
|
return ct, fmt.Errorf("failed to submit magnet: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the torrent was submitted
|
// Check if the torrent was submitted
|
||||||
if newTorrent == nil || newTorrent.Id == "" {
|
if newTorrent == nil || newTorrent.Id == "" {
|
||||||
c.failedToReinsert.Store(oldID, struct{}{})
|
c.markAsFailedToReinsert(oldID)
|
||||||
return ct, fmt.Errorf("failed to submit magnet: empty torrent")
|
return ct, fmt.Errorf("failed to submit magnet: empty torrent")
|
||||||
}
|
}
|
||||||
newTorrent.DownloadUncached = false // Set to false, avoid re-downloading
|
newTorrent.DownloadUncached = false // Set to false, avoid re-downloading
|
||||||
@@ -178,7 +195,7 @@ func (c *Cache) reInsertTorrent(ct *CachedTorrent) (*CachedTorrent, error) {
|
|||||||
// Delete the torrent if it was not downloaded
|
// Delete the torrent if it was not downloaded
|
||||||
_ = c.client.DeleteTorrent(newTorrent.Id)
|
_ = c.client.DeleteTorrent(newTorrent.Id)
|
||||||
}
|
}
|
||||||
c.failedToReinsert.Store(oldID, struct{}{})
|
c.markAsFailedToReinsert(oldID)
|
||||||
return ct, err
|
return ct, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -189,7 +206,7 @@ func (c *Cache) reInsertTorrent(ct *CachedTorrent) (*CachedTorrent, error) {
|
|||||||
}
|
}
|
||||||
for _, f := range newTorrent.Files {
|
for _, f := range newTorrent.Files {
|
||||||
if f.Link == "" {
|
if f.Link == "" {
|
||||||
c.failedToReinsert.Store(oldID, struct{}{})
|
c.markAsFailedToReinsert(oldID)
|
||||||
return ct, fmt.Errorf("failed to reinsert torrent: empty link")
|
return ct, fmt.Errorf("failed to reinsert torrent: empty link")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package debrid
|
package debrid
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"regexp"
|
"regexp"
|
||||||
"sort"
|
"sort"
|
||||||
@@ -51,9 +52,11 @@ type torrentCache struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type sortableFile struct {
|
type sortableFile struct {
|
||||||
|
id string
|
||||||
name string
|
name string
|
||||||
modTime time.Time
|
modTime time.Time
|
||||||
size int64
|
size int64
|
||||||
|
bad bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func newTorrentCache(dirFilters map[string][]directoryFilter) *torrentCache {
|
func newTorrentCache(dirFilters map[string][]directoryFilter) *torrentCache {
|
||||||
@@ -132,7 +135,7 @@ func (tc *torrentCache) refreshListing() {
|
|||||||
tc.mu.Lock()
|
tc.mu.Lock()
|
||||||
all := make([]sortableFile, 0, len(tc.byName))
|
all := make([]sortableFile, 0, len(tc.byName))
|
||||||
for name, t := range tc.byName {
|
for name, t := range tc.byName {
|
||||||
all = append(all, sortableFile{name, t.AddedOn, t.Size})
|
all = append(all, sortableFile{t.Id, name, t.AddedOn, t.Size, t.Bad})
|
||||||
}
|
}
|
||||||
tc.sortNeeded.Store(false)
|
tc.sortNeeded.Store(false)
|
||||||
tc.mu.Unlock()
|
tc.mu.Unlock()
|
||||||
@@ -156,6 +159,30 @@ func (tc *torrentCache) refreshListing() {
|
|||||||
}()
|
}()
|
||||||
wg.Done()
|
wg.Done()
|
||||||
|
|
||||||
|
wg.Add(1)
|
||||||
|
// For __bad__
|
||||||
|
go func() {
|
||||||
|
listing := make([]os.FileInfo, 0)
|
||||||
|
for _, sf := range all {
|
||||||
|
if sf.bad {
|
||||||
|
listing = append(listing, &fileInfo{
|
||||||
|
name: fmt.Sprintf("%s(%s)", sf.name, sf.id),
|
||||||
|
size: sf.size,
|
||||||
|
mode: 0755 | os.ModeDir,
|
||||||
|
modTime: sf.modTime,
|
||||||
|
isDir: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tc.folderListingMu.Lock()
|
||||||
|
if len(listing) > 0 {
|
||||||
|
tc.folderListing["__bad__"] = listing
|
||||||
|
} else {
|
||||||
|
delete(tc.folderListing, "__bad__")
|
||||||
|
}
|
||||||
|
tc.folderListingMu.Unlock()
|
||||||
|
}()
|
||||||
|
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
wg.Add(len(tc.directoriesFilters)) // for each directory filter
|
wg.Add(len(tc.directoriesFilters)) // for each directory filter
|
||||||
for dir, filters := range tc.directoriesFilters {
|
for dir, filters := range tc.directoriesFilters {
|
||||||
|
|||||||
@@ -9,33 +9,26 @@ import (
|
|||||||
func (c *Cache) StartSchedule() error {
|
func (c *Cache) StartSchedule() error {
|
||||||
// For now, we just want to refresh the listing and download links
|
// For now, we just want to refresh the listing and download links
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
downloadLinkJob, err := utils.ScheduleJob(ctx, c.downloadLinksRefreshInterval, nil, c.refreshDownloadLinks)
|
|
||||||
if err != nil {
|
if _, err := utils.ScheduleJob(ctx, c.downloadLinksRefreshInterval, nil, c.refreshDownloadLinks); err != nil {
|
||||||
c.logger.Error().Err(err).Msg("Failed to add download link refresh job")
|
c.logger.Error().Err(err).Msg("Failed to add download link refresh job")
|
||||||
}
|
} else {
|
||||||
if t, err := downloadLinkJob.NextRun(); err == nil {
|
c.logger.Debug().Msgf("Download link refresh job scheduled every %s", c.downloadLinksRefreshInterval)
|
||||||
c.logger.Trace().Msgf("Next download link refresh job: %s", t.Format("2006-01-02 15:04:05"))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
torrentJob, err := utils.ScheduleJob(ctx, c.torrentRefreshInterval, nil, c.refreshTorrents)
|
if _, err := utils.ScheduleJob(ctx, c.torrentRefreshInterval, nil, c.refreshTorrents); err != nil {
|
||||||
if err != nil {
|
|
||||||
c.logger.Error().Err(err).Msg("Failed to add torrent refresh job")
|
c.logger.Error().Err(err).Msg("Failed to add torrent refresh job")
|
||||||
}
|
} else {
|
||||||
if t, err := torrentJob.NextRun(); err == nil {
|
c.logger.Debug().Msgf("Torrent refresh job scheduled every %s", c.torrentRefreshInterval)
|
||||||
c.logger.Trace().Msgf("Next torrent refresh job: %s", t.Format("2006-01-02 15:04:05"))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Schedule the reset invalid links job
|
// Schedule the reset invalid links job
|
||||||
// This job will run every 24 hours
|
// This job will run every at 00:00 CET
|
||||||
// and reset the invalid links in the cache
|
// and reset the invalid links in the cache
|
||||||
cet, _ := time.LoadLocation("CET")
|
cet, _ := time.LoadLocation("CET")
|
||||||
resetLinksJob, err := utils.ScheduleJob(ctx, "00:00", cet, c.resetInvalidLinks)
|
if _, err := utils.ScheduleJob(ctx, "00:00", cet, c.resetInvalidLinks); err != nil {
|
||||||
if err != nil {
|
|
||||||
c.logger.Error().Err(err).Msg("Failed to add reset invalid links job")
|
c.logger.Error().Err(err).Msg("Failed to add reset invalid links job")
|
||||||
}
|
}
|
||||||
if t, err := resetLinksJob.NextRun(); err == nil {
|
|
||||||
c.logger.Trace().Msgf("Next reset invalid download links job at: %s", t.Format("2006-01-02 15:04:05"))
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -281,13 +281,6 @@
|
|||||||
</div>
|
</div>
|
||||||
<small class="form-text text-muted">Run repair on startup</small>
|
<small class="form-text text-muted">Run repair on startup</small>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3 mb-3">
|
|
||||||
<div class="form-check">
|
|
||||||
<input type="checkbox" class="form-check-input" name="repair.reinsert" id="repair.reinsert">
|
|
||||||
<label class="form-check-label" for="repair.reinsert">Re-Insert Nerfed Release</label>
|
|
||||||
</div>
|
|
||||||
<small class="form-text text-muted">Tries to autofix broken releases by re-inserting the torrent</small>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-3 mb-3">
|
<div class="col-md-3 mb-3">
|
||||||
<div class="form-check">
|
<div class="form-check">
|
||||||
<input type="checkbox" class="form-check-input" name="repair.auto_process" id="repair.auto_process">
|
<input type="checkbox" class="form-check-input" name="repair.auto_process" id="repair.auto_process">
|
||||||
@@ -1063,7 +1056,6 @@
|
|||||||
enabled: document.querySelector('[name="repair.enabled"]').checked,
|
enabled: document.querySelector('[name="repair.enabled"]').checked,
|
||||||
interval: document.querySelector('[name="repair.interval"]').value,
|
interval: document.querySelector('[name="repair.interval"]').value,
|
||||||
run_on_start: document.querySelector('[name="repair.run_on_start"]').checked,
|
run_on_start: document.querySelector('[name="repair.run_on_start"]').checked,
|
||||||
reinsert: document.querySelector('[name="repair.reinsert"]').checked,
|
|
||||||
zurg_url: document.querySelector('[name="repair.zurg_url"]').value,
|
zurg_url: document.querySelector('[name="repair.zurg_url"]').value,
|
||||||
use_webdav: document.querySelector('[name="repair.use_webdav"]').checked,
|
use_webdav: document.querySelector('[name="repair.use_webdav"]').checked,
|
||||||
auto_process: document.querySelector('[name="repair.auto_process"]').checked
|
auto_process: document.querySelector('[name="repair.auto_process"]').checked
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ func (h *Handler) getTorrentsFolders(folder string) []os.FileInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) getParentItems() []string {
|
func (h *Handler) getParentItems() []string {
|
||||||
parents := []string{"__all__", "torrents"}
|
parents := []string{"__all__", "torrents", "__bad__"}
|
||||||
|
|
||||||
// Add custom folders
|
// Add custom folders
|
||||||
parents = append(parents, h.cache.GetCustomFolders()...)
|
parents = append(parents, h.cache.GetCustomFolders()...)
|
||||||
@@ -122,7 +122,7 @@ func (h *Handler) getChildren(name string) []os.FileInfo {
|
|||||||
name = utils.PathUnescape(path.Clean(name))
|
name = utils.PathUnescape(path.Clean(name))
|
||||||
root := path.Clean(h.getRootPath())
|
root := path.Clean(h.getRootPath())
|
||||||
|
|
||||||
// top‐level “parents” (e.g. __all__, torrents)
|
// top‐level “parents” (e.g. __all__, torrents etc)
|
||||||
if name == root {
|
if name == root {
|
||||||
return h.getParentFiles()
|
return h.getParentFiles()
|
||||||
}
|
}
|
||||||
@@ -397,6 +397,7 @@ func (h *Handler) serveDirectory(w http.ResponseWriter, r *http.Request, file we
|
|||||||
}
|
}
|
||||||
return fmt.Sprintf("%.2f %s", size, unit)
|
return fmt.Sprintf("%.2f %s", size, unit)
|
||||||
},
|
},
|
||||||
|
"hasSuffix": strings.HasSuffix,
|
||||||
}
|
}
|
||||||
tmpl, err := template.New("directory").Funcs(funcMap).Parse(directoryTemplate)
|
tmpl, err := template.New("directory").Funcs(funcMap).Parse(directoryTemplate)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -443,7 +444,7 @@ func (h *Handler) handleGet(w http.ResponseWriter, r *http.Request) {
|
|||||||
if file, ok := fRaw.(*File); ok && file.content == nil {
|
if file, ok := fRaw.(*File); ok && file.content == nil {
|
||||||
link, err := file.getDownloadLink()
|
link, err := file.getDownloadLink()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
h.logger.Trace().
|
h.logger.Error().
|
||||||
Err(err).
|
Err(err).
|
||||||
Str("path", r.URL.Path).
|
Str("path", r.URL.Path).
|
||||||
Msg("Could not fetch download link")
|
Msg("Could not fetch download link")
|
||||||
|
|||||||
@@ -103,6 +103,19 @@ const directoryTemplate = `
|
|||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
.disabled {
|
||||||
|
color: #999;
|
||||||
|
pointer-events: none;
|
||||||
|
border-color: #f0f0f0;
|
||||||
|
background-color: #f8f8f8;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
.disabled .file-number {
|
||||||
|
color: #bbb;
|
||||||
|
}
|
||||||
|
.disabled .file-info {
|
||||||
|
color: #aaa;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -111,9 +124,14 @@ const directoryTemplate = `
|
|||||||
{{if .ShowParent}}
|
{{if .ShowParent}}
|
||||||
<li><a href="{{urlpath .ParentPath}}" class="parent-dir"><span class="file-number"></span>Parent Directory</a></li>
|
<li><a href="{{urlpath .ParentPath}}" class="parent-dir"><span class="file-number"></span>Parent Directory</a></li>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
{{$isBadPath := hasSuffix .Path "__bad__"}}
|
||||||
{{range $index, $file := .Children}}
|
{{range $index, $file := .Children}}
|
||||||
<li>
|
<li>
|
||||||
<a href="{{urlpath (printf "%s/%s" $.Path $file.Name)}}">
|
{{if $isBadPath}}
|
||||||
|
<a class="disabled">
|
||||||
|
{{else}}
|
||||||
|
<a href="{{urlpath (printf "%s/%s" $.Path $file.Name)}}">
|
||||||
|
{{end}}
|
||||||
<span class="file-number">{{add $index 1}}.</span>
|
<span class="file-number">{{add $index 1}}.</span>
|
||||||
<span class="file-name">{{$file.Name}}{{if $file.IsDir}}/{{end}}</span>
|
<span class="file-name">{{$file.Name}}{{if $file.IsDir}}/{{end}}</span>
|
||||||
<span class="file-info">
|
<span class="file-info">
|
||||||
|
|||||||
Reference in New Issue
Block a user