#lang scheme/base (require racket/file racket/path setup/dirs) (provide links) (define (links #:error [error error] #:user? [user? #t] #:file [in-file #f] #:name [name #f] #:version-regexp [version-regexp #f] #:root? [root? #f] #:remove? [remove? #f] #:show? [show? #f] #:repair? [repair? #f] #:with-path? [with-path? #f] . dirs) (define (check-name name) (unless (and (regexp-match #rx"^[a-zA-z0-9+_%-]+$" name) (module-path? name)) (error 'links "name is not valid as a top-level collection name: ~e" name))) (when name (check-name name)) (define file (or in-file (if user? (find-system-path 'links-file) (let ([d (find-collects-dir)]) (if d (build-path d "config" "links.rktd") (error 'links "cannot find installation collections path")))))) (define need-repair? #f) (define (content-error str v) (if repair? (begin (log-warning (format "~a~e" str v)) (set! need-repair? #t) #f) (error 'links "~a~e" str v))) (define table (with-handlers ([exn:fail? (lambda (exn) (let ([msg (format "error reading from link file: ~s: ~a" file (exn-message exn))]) (if repair? (begin (log-warning msg) (set! need-repair? #t) null) (error 'links "~a" msg))))]) (if (file-exists? file) (let ([l (with-input-from-file file read)]) (if (list? l) (for/list ([e (in-list l)] #:when (or (and (list? e) (or (= 2 (length e)) (= 3 (length e)))) (content-error "entry is a not a 2- or 3-element list: " e)) #:when (or (or (string? (car e)) (eq? 'root (car e))) (content-error "entry's first element is not a string or 'root: " e)) #:when (or (path-string? (cadr e)) (content-error "entry's second element is not a path string: " e)) #:when (or (null? (cddr e)) (regexp? (caddr e)) (content-error "entry's third element is not a version regexp: " e))) e) (begin (content-error "content is not a list: " l) null))) null))) (define mapped (make-hash)) (define (add-entry! e) (hash-set! mapped (car e) (cons (cdr e) (hash-ref mapped (car e) null)))) (for ([e (in-list table)]) (add-entry! e)) (define file-dir (let-values ([(base name dir?) (split-path (simplify-path (path->complete-path file)))]) base)) (define (simplify p) (simplify-path (path->complete-path p file-dir))) (define new-table (reverse (for/fold ([table (reverse table)]) ([d (in-list (if (and (null? dirs) name) '(#f) dirs))]) (let* ([dp (and d (find-relative-path file-dir (simplify-path (path->complete-path d)) #:more-than-root? #t))] [a-name (if root? 'root (and d (or name (let-values ([(base name dir?) (split-path dp)]) (path-element->string name)))))] [rx version-regexp] [d (and dp (path->string dp))] [sd (and d (simplify d))]) (unless remove? (unless (directory-exists? sd) (error 'links "no such directory for link: ~a" sd))) (if remove? (filter (lambda (e) (or (and d (not (equal? (simplify (cadr e)) sd))) (and name (not (equal? (car e) name))) (and root? (not (eq? (car e) 'root))) (and version-regexp (pair? (cddr e)) (not (equal? (caddr e) version-regexp))))) table) (let ([l (hash-ref mapped a-name null)] [e (list* a-name d (if rx (list rx) null))]) (if (member (cdr e) l) table (let () (when (string? a-name) (check-name a-name)) (add-entry! e) (cons e table))))))))) (unless (and (not need-repair?) (equal? new-table table)) (let ([dir (let-values ([(base name dir?) (split-path file)]) base)]) (make-directory* dir) (let ([tmp (make-temporary-file "links~a.rktd" #f dir)]) (with-output-to-file tmp #:exists 'truncate (lambda () (printf "(") (let loop ([l new-table] [prefix ""]) (cond [(null? l) (printf ")\n")] [else (printf "~a~s" prefix (car l)) (unless (null? (cdr l)) (newline)) (loop (cdr l) " ")])))) (with-handlers ([exn:fail? (lambda (exn) (with-handlers ([exn:fail? void]) (delete-file tmp)) (raise exn))]) (rename-file-or-directory tmp file #t))))) (when show? (for ([e (in-list new-table)]) (printf " ~a~s path: ~s~a\n" (if (eq? (car e) 'root) "" "collection: ") (car e) (path->string (simplify (cadr e))) (if (null? (cddr e)) "" (format " version: ~s" (caddr e)))))) (if remove? ;; return list of removed entries: (filter (lambda (e) (not (member e new-table))) table) (if root? ;; Return root paths: (for/list ([e (in-list new-table)] #:when (eq? 'root (car e))) (simplify (cadr e))) ;; Return list of collections mapped for this version: (let ([ht (make-hash)]) (for ([e (in-list new-table)]) (when (and (string? (car e)) (or (null? (cddr e)) (regexp-match? (caddr e) (version)))) (hash-set! ht (car e) (cadr e)))) (hash-map ht (lambda (k p) (if with-path? (cons k (simplify p)) k)))))))