2016-08-22 18:31:50 -06:00
|
|
|
package bind
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
|
|
|
bind -
|
2020-02-22 13:27:24 -05:00
|
|
|
Generate zonefiles suitable for BIND.
|
2016-08-22 18:31:50 -06:00
|
|
|
|
|
|
|
The zonefiles are read and written to the directory -bind_dir
|
|
|
|
|
|
|
|
If the old zonefiles are readable, we read them to determine
|
|
|
|
if an update is actually needed. The old zonefile is also used
|
|
|
|
as the basis for generating the new SOA serial number.
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
import (
|
2017-03-14 14:47:40 -07:00
|
|
|
"bytes"
|
2016-08-22 18:31:50 -06:00
|
|
|
"encoding/json"
|
|
|
|
"fmt"
|
|
|
|
"os"
|
|
|
|
"path/filepath"
|
|
|
|
"strings"
|
2020-02-22 13:27:24 -05:00
|
|
|
"time"
|
2016-08-22 18:31:50 -06:00
|
|
|
|
2023-05-20 19:21:45 +02:00
|
|
|
"github.com/StackExchange/dnscontrol/v4/models"
|
|
|
|
"github.com/StackExchange/dnscontrol/v4/pkg/diff"
|
|
|
|
"github.com/StackExchange/dnscontrol/v4/pkg/diff2"
|
|
|
|
"github.com/StackExchange/dnscontrol/v4/pkg/prettyzone"
|
|
|
|
"github.com/StackExchange/dnscontrol/v4/pkg/printer"
|
2023-06-20 12:35:11 -04:00
|
|
|
"github.com/StackExchange/dnscontrol/v4/pkg/txtutil"
|
2023-05-20 19:21:45 +02:00
|
|
|
"github.com/StackExchange/dnscontrol/v4/providers"
|
2022-08-14 20:46:56 -04:00
|
|
|
"github.com/miekg/dns"
|
2016-08-22 18:31:50 -06:00
|
|
|
)
|
|
|
|
|
2018-01-04 19:19:35 -05:00
|
|
|
var features = providers.DocumentationNotes{
|
2021-05-07 14:39:26 -04:00
|
|
|
providers.CanAutoDNSSEC: providers.Can("Just writes out a comment indicating DNSSEC was requested"),
|
|
|
|
providers.CanGetZones: providers.Can(),
|
2018-01-04 19:19:35 -05:00
|
|
|
providers.CanUseCAA: providers.Can(),
|
2023-08-22 11:09:50 +02:00
|
|
|
providers.CanUseDHCID: providers.Can(),
|
2020-05-30 10:40:21 -04:00
|
|
|
providers.CanUseDS: providers.Can(),
|
2023-03-16 19:04:20 +01:00
|
|
|
providers.CanUseLOC: providers.Can(),
|
2019-03-29 12:01:52 +01:00
|
|
|
providers.CanUseNAPTR: providers.Can(),
|
2021-05-07 14:39:26 -04:00
|
|
|
providers.CanUsePTR: providers.Can(),
|
|
|
|
providers.CanUseSOA: providers.Can(),
|
2018-01-04 19:19:35 -05:00
|
|
|
providers.CanUseSRV: providers.Can(),
|
2019-01-28 23:26:20 +01:00
|
|
|
providers.CanUseSSHFP: providers.Can(),
|
2018-01-04 19:19:35 -05:00
|
|
|
providers.CanUseTLSA: providers.Can(),
|
|
|
|
providers.CantUseNOPURGE: providers.Cannot(),
|
2017-09-14 16:13:17 -04:00
|
|
|
providers.DocCreateDomains: providers.Can("Driver just maintains list of zone files. It should automatically add missing ones."),
|
2018-01-04 19:19:35 -05:00
|
|
|
providers.DocDualHost: providers.Can(),
|
2017-09-14 16:13:17 -04:00
|
|
|
providers.DocOfficiallySupported: providers.Can(),
|
|
|
|
}
|
|
|
|
|
2017-07-06 10:24:21 -04:00
|
|
|
func initBind(config map[string]string, providermeta json.RawMessage) (providers.DNSServiceProvider, error) {
|
|
|
|
// config -- the key/values from creds.json
|
|
|
|
// meta -- the json blob from NewReq('name', 'TYPE', meta)
|
2020-10-26 09:25:30 -04:00
|
|
|
api := &bindProvider{
|
2021-02-05 12:12:45 -05:00
|
|
|
directory: config["directory"],
|
|
|
|
filenameformat: config["filenameformat"],
|
2017-09-13 10:00:41 -04:00
|
|
|
}
|
|
|
|
if api.directory == "" {
|
|
|
|
api.directory = "zones"
|
|
|
|
}
|
2021-02-05 12:12:45 -05:00
|
|
|
if api.filenameformat == "" {
|
|
|
|
api.filenameformat = "%U.zone"
|
|
|
|
}
|
2017-07-06 10:24:21 -04:00
|
|
|
if len(providermeta) != 0 {
|
|
|
|
err := json.Unmarshal(providermeta, api)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
}
|
2020-03-01 10:33:24 -05:00
|
|
|
var nss []string
|
2022-02-15 16:01:25 -05:00
|
|
|
for i, ns := range api.DefaultNS {
|
|
|
|
if ns == "" {
|
|
|
|
return nil, fmt.Errorf("empty string in default_ns[%d]", i)
|
|
|
|
}
|
|
|
|
// If it contains a ".", it must end in a ".".
|
|
|
|
if strings.ContainsRune(ns, '.') && ns[len(ns)-1] != '.' {
|
2023-01-29 19:14:22 +01:00
|
|
|
return nil, fmt.Errorf("default_ns (%v) must end with a (.) [https://docs.dnscontrol.org/language-reference/why-the-dot]", ns)
|
2022-02-15 16:01:25 -05:00
|
|
|
}
|
|
|
|
// This is one of the (increasingly rare) cases where we store a
|
|
|
|
// name without the trailing dot to indicate a FQDN.
|
|
|
|
nss = append(nss, strings.TrimSuffix(ns, "."))
|
2020-03-01 10:33:24 -05:00
|
|
|
}
|
|
|
|
var err error
|
|
|
|
api.nameservers, err = models.ToNameservers(nss)
|
|
|
|
return api, err
|
2017-07-06 10:24:21 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
func init() {
|
2021-03-07 13:19:22 -05:00
|
|
|
fns := providers.DspFuncs{
|
2021-05-04 14:15:31 -04:00
|
|
|
Initializer: initBind,
|
2021-03-08 20:14:30 -05:00
|
|
|
RecordAuditor: AuditRecords,
|
2021-03-07 13:19:22 -05:00
|
|
|
}
|
|
|
|
providers.RegisterDomainServiceProviderType("BIND", fns, features)
|
2017-07-06 10:24:21 -04:00
|
|
|
}
|
|
|
|
|
2021-05-07 14:39:26 -04:00
|
|
|
// SoaDefaults contains the parts of the default SOA settings.
|
|
|
|
type SoaDefaults struct {
|
2016-08-22 18:31:50 -06:00
|
|
|
Ns string `json:"master"`
|
|
|
|
Mbox string `json:"mbox"`
|
|
|
|
Serial uint32 `json:"serial"`
|
|
|
|
Refresh uint32 `json:"refresh"`
|
|
|
|
Retry uint32 `json:"retry"`
|
|
|
|
Expire uint32 `json:"expire"`
|
|
|
|
Minttl uint32 `json:"minttl"`
|
2020-08-20 14:44:15 -05:00
|
|
|
TTL uint32 `json:"ttl,omitempty"`
|
2016-08-22 18:31:50 -06:00
|
|
|
}
|
|
|
|
|
2021-05-07 14:39:26 -04:00
|
|
|
func (s SoaDefaults) String() string {
|
2020-08-20 14:44:15 -05:00
|
|
|
return fmt.Sprintf("%s %s %d %d %d %d %d %d", s.Ns, s.Mbox, s.Serial, s.Refresh, s.Retry, s.Expire, s.Minttl, s.TTL)
|
2016-08-22 18:31:50 -06:00
|
|
|
}
|
|
|
|
|
2020-10-26 09:25:30 -04:00
|
|
|
// bindProvider is the provider handle for the bindProvider driver.
|
|
|
|
type bindProvider struct {
|
2023-03-22 08:46:36 -07:00
|
|
|
DefaultNS []string `json:"default_ns"`
|
|
|
|
DefaultSoa SoaDefaults `json:"default_soa"`
|
|
|
|
nameservers []*models.Nameserver
|
|
|
|
directory string
|
|
|
|
filenameformat string
|
|
|
|
zonefile string // Where the zone data is e texpected
|
|
|
|
zoneFileFound bool // Did the zonefile exist?
|
|
|
|
skipNextSoaIncrease bool // skip next SOA increment (for testing only)
|
2018-02-15 12:02:50 -05:00
|
|
|
}
|
|
|
|
|
2018-01-09 12:53:16 -05:00
|
|
|
// GetNameservers returns the nameservers for a domain.
|
2020-10-26 09:25:30 -04:00
|
|
|
func (c *bindProvider) GetNameservers(string) ([]*models.Nameserver, error) {
|
2020-03-26 09:59:59 -04:00
|
|
|
var r []string
|
|
|
|
for _, j := range c.nameservers {
|
|
|
|
r = append(r, j.Name)
|
|
|
|
}
|
|
|
|
return models.ToNameservers(r)
|
2016-08-22 18:31:50 -06:00
|
|
|
}
|
|
|
|
|
2020-02-21 13:48:55 -05:00
|
|
|
// ListZones returns all the zones in an account
|
2020-10-26 09:25:30 -04:00
|
|
|
func (c *bindProvider) ListZones() ([]string, error) {
|
2020-02-21 13:48:55 -05:00
|
|
|
if _, err := os.Stat(c.directory); os.IsNotExist(err) {
|
2020-02-23 13:58:49 -05:00
|
|
|
return nil, fmt.Errorf("directory %q does not exist", c.directory)
|
2020-02-21 13:48:55 -05:00
|
|
|
}
|
|
|
|
|
2021-02-05 12:12:45 -05:00
|
|
|
var files []string
|
|
|
|
f, err := os.Open(c.directory)
|
2020-02-21 13:48:55 -05:00
|
|
|
if err != nil {
|
2021-02-05 12:12:45 -05:00
|
|
|
return files, fmt.Errorf("bind ListZones open dir %q: %w",
|
|
|
|
c.directory, err)
|
2020-02-21 13:48:55 -05:00
|
|
|
}
|
2021-02-05 12:12:45 -05:00
|
|
|
filenames, err := f.Readdirnames(-1)
|
|
|
|
if err != nil {
|
|
|
|
return files, fmt.Errorf("bind ListZones readdir %q: %w",
|
|
|
|
c.directory, err)
|
2020-02-21 13:48:55 -05:00
|
|
|
}
|
2021-02-05 12:12:45 -05:00
|
|
|
|
|
|
|
return extractZonesFromFilenames(c.filenameformat, filenames), nil
|
2020-02-21 13:48:55 -05:00
|
|
|
}
|
|
|
|
|
2020-02-18 08:59:18 -05:00
|
|
|
// GetZoneRecords gets the records of a zone and returns them in RecordConfig format.
|
2023-05-02 13:04:59 -04:00
|
|
|
func (c *bindProvider) GetZoneRecords(domain string, meta map[string]string) (models.Records, error) {
|
2018-12-10 14:05:01 -05:00
|
|
|
|
|
|
|
if _, err := os.Stat(c.directory); os.IsNotExist(err) {
|
2023-05-26 13:16:20 -04:00
|
|
|
printer.Printf("\nWARNING: BIND directory %q does not exist! (will create)\n", c.directory)
|
2018-12-10 14:05:01 -05:00
|
|
|
}
|
2023-05-31 18:14:04 +02:00
|
|
|
_, okTag := meta[models.DomainTag]
|
|
|
|
_, okUnique := meta[models.DomainUniqueName]
|
|
|
|
if !okTag && !okUnique {
|
2021-02-05 12:12:45 -05:00
|
|
|
// This layering violation is needed for tests only.
|
|
|
|
// Otherwise, this is set already.
|
2023-05-02 13:04:59 -04:00
|
|
|
// Note: In this situation there is no "uniquename" or "tag".
|
2021-02-05 12:12:45 -05:00
|
|
|
c.zonefile = filepath.Join(c.directory,
|
|
|
|
makeFileName(c.filenameformat, domain, domain, ""))
|
2023-05-31 18:14:04 +02:00
|
|
|
} else {
|
|
|
|
c.zonefile = filepath.Join(c.directory,
|
|
|
|
makeFileName(c.filenameformat,
|
|
|
|
meta[models.DomainUniqueName], domain, meta[models.DomainTag]),
|
|
|
|
)
|
2021-02-05 12:12:45 -05:00
|
|
|
}
|
2022-08-14 12:50:15 -04:00
|
|
|
content, err := os.ReadFile(c.zonefile)
|
2020-02-23 13:58:49 -05:00
|
|
|
if os.IsNotExist(err) {
|
|
|
|
// If the file doesn't exist, that's not an error. Just informational.
|
|
|
|
c.zoneFileFound = false
|
2022-12-11 17:28:58 -05:00
|
|
|
fmt.Fprintf(os.Stderr, "File does not yet exist: %q (will create)\n", c.zonefile)
|
2020-02-23 13:58:49 -05:00
|
|
|
return nil, nil
|
2016-08-22 18:31:50 -06:00
|
|
|
}
|
2020-02-23 13:58:49 -05:00
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("can't open %s: %w", c.zonefile, err)
|
|
|
|
}
|
|
|
|
c.zoneFileFound = true
|
2016-08-22 18:31:50 -06:00
|
|
|
|
2023-02-19 12:33:08 -05:00
|
|
|
zonefileName := c.zonefile
|
|
|
|
|
|
|
|
return ParseZoneContents(string(content), domain, zonefileName)
|
|
|
|
}
|
2020-02-23 13:58:49 -05:00
|
|
|
|
2023-02-27 20:28:17 -05:00
|
|
|
// ParseZoneContents parses a string as a BIND zone and returns the records.
|
2023-02-19 12:33:08 -05:00
|
|
|
func ParseZoneContents(content string, zoneName string, zonefileName string) (models.Records, error) {
|
|
|
|
zp := dns.NewZoneParser(strings.NewReader(content), zoneName, zonefileName)
|
|
|
|
|
|
|
|
foundRecords := models.Records{}
|
2020-02-23 13:58:49 -05:00
|
|
|
for rr, ok := zp.Next(); ok; rr, ok = zp.Next() {
|
2023-02-19 12:33:08 -05:00
|
|
|
rec, err := models.RRtoRC(rr, zoneName)
|
2021-07-06 17:03:29 +02:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2020-02-23 13:58:49 -05:00
|
|
|
foundRecords = append(foundRecords, &rec)
|
2020-02-18 08:59:18 -05:00
|
|
|
}
|
|
|
|
|
2020-02-23 13:58:49 -05:00
|
|
|
if err := zp.Err(); err != nil {
|
2023-02-19 12:33:08 -05:00
|
|
|
return nil, fmt.Errorf("error while parsing '%v': %w", zonefileName, err)
|
2020-02-23 13:58:49 -05:00
|
|
|
}
|
2020-02-18 08:59:18 -05:00
|
|
|
return foundRecords, nil
|
|
|
|
}
|
|
|
|
|
2023-04-14 15:22:23 -04:00
|
|
|
// GetZoneRecordsCorrections returns a list of corrections that will turn existing records into dc.Records.
|
|
|
|
func (c *bindProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, foundRecords models.Records) ([]*models.Correction, error) {
|
2023-06-20 12:35:11 -04:00
|
|
|
txtutil.SplitSingleLongTxt(dc.Records)
|
2021-02-05 12:12:45 -05:00
|
|
|
|
2023-04-14 15:22:23 -04:00
|
|
|
changes := false
|
|
|
|
var msg string
|
2020-02-23 13:58:49 -05:00
|
|
|
|
|
|
|
// Find the SOA records; use them to make or update the desired SOA.
|
|
|
|
var foundSoa *models.RecordConfig
|
|
|
|
for _, r := range foundRecords {
|
|
|
|
if r.Type == "SOA" && r.Name == "@" {
|
|
|
|
foundSoa = r
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
var desiredSoa *models.RecordConfig
|
|
|
|
for _, r := range dc.Records {
|
|
|
|
if r.Type == "SOA" && r.Name == "@" {
|
|
|
|
desiredSoa = r
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
soaRec, nextSerial := makeSoa(dc.Name, &c.DefaultSoa, foundSoa, desiredSoa)
|
|
|
|
if desiredSoa == nil {
|
|
|
|
dc.Records = append(dc.Records, soaRec)
|
|
|
|
desiredSoa = dc.Records[len(dc.Records)-1]
|
|
|
|
} else {
|
|
|
|
*desiredSoa = *soaRec
|
2023-03-22 08:46:36 -07:00
|
|
|
c.skipNextSoaIncrease = true
|
2020-02-22 13:27:24 -05:00
|
|
|
}
|
2016-08-22 18:31:50 -06:00
|
|
|
|
2022-12-11 17:28:58 -05:00
|
|
|
if !diff2.EnableDiff2 {
|
2016-08-22 18:31:50 -06:00
|
|
|
|
2022-12-11 15:02:58 -05:00
|
|
|
differ := diff.New(dc)
|
|
|
|
_, create, del, mod, err := differ.IncrementalDiff(foundRecords)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
2016-08-22 18:31:50 -06:00
|
|
|
}
|
2022-12-11 15:02:58 -05:00
|
|
|
|
|
|
|
buf := &bytes.Buffer{}
|
|
|
|
// Print a list of changes. Generate an actual change that is the zone
|
2022-12-11 17:28:58 -05:00
|
|
|
|
2022-12-11 15:02:58 -05:00
|
|
|
for _, i := range create {
|
|
|
|
changes = true
|
|
|
|
if c.zoneFileFound {
|
|
|
|
fmt.Fprintln(buf, i)
|
|
|
|
}
|
2016-08-22 18:31:50 -06:00
|
|
|
}
|
2022-12-11 15:02:58 -05:00
|
|
|
for _, i := range del {
|
|
|
|
changes = true
|
|
|
|
if c.zoneFileFound {
|
|
|
|
fmt.Fprintln(buf, i)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
for _, i := range mod {
|
|
|
|
changes = true
|
|
|
|
if c.zoneFileFound {
|
|
|
|
fmt.Fprintln(buf, i)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-02-18 08:59:18 -05:00
|
|
|
if c.zoneFileFound {
|
2022-12-11 15:02:58 -05:00
|
|
|
msg = fmt.Sprintf("GENERATE_ZONEFILE: '%s'. Changes:\n%s", dc.Name, buf)
|
|
|
|
} else {
|
|
|
|
msg = fmt.Sprintf("GENERATE_ZONEFILE: '%s' (new file with %d records)\n", dc.Name, len(create))
|
2016-08-22 18:31:50 -06:00
|
|
|
}
|
2020-02-23 13:58:49 -05:00
|
|
|
|
2022-12-11 17:28:58 -05:00
|
|
|
} else {
|
|
|
|
|
|
|
|
var msgs []string
|
2023-04-14 15:22:23 -04:00
|
|
|
var err error
|
2022-12-11 17:28:58 -05:00
|
|
|
msgs, changes, err = diff2.ByZone(foundRecords, dc, nil)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
2022-12-11 15:02:58 -05:00
|
|
|
}
|
2022-12-11 17:28:58 -05:00
|
|
|
//fmt.Printf("DEBUG: BIND changes=%v\n", changes)
|
|
|
|
msg = strings.Join(msgs, "\n")
|
2020-02-23 13:58:49 -05:00
|
|
|
|
2016-08-22 18:31:50 -06:00
|
|
|
}
|
|
|
|
|
2022-12-11 17:28:58 -05:00
|
|
|
var corrections []*models.Correction
|
|
|
|
//fmt.Printf("DEBUG: BIND changes=%v\n", changes)
|
|
|
|
if changes {
|
|
|
|
|
2023-04-14 15:22:23 -04:00
|
|
|
comments := make([]string, 0, 5)
|
|
|
|
comments = append(comments,
|
|
|
|
fmt.Sprintf("generated with dnscontrol %s", time.Now().Format(time.RFC3339)),
|
|
|
|
)
|
|
|
|
if dc.AutoDNSSEC == "on" {
|
|
|
|
// This does nothing but reminds the user to add the correct
|
|
|
|
// auto-dnssecc zone statement to named.conf.
|
|
|
|
// While it is a no-op, it is useful for situations where a zone
|
|
|
|
// has multiple providers.
|
|
|
|
comments = append(comments, "Automatic DNSSEC signing requested")
|
|
|
|
}
|
|
|
|
|
2023-04-17 16:44:10 +02:00
|
|
|
c.zonefile = filepath.Join(c.directory,
|
2023-05-02 13:04:59 -04:00
|
|
|
makeFileName(c.filenameformat,
|
2023-05-08 23:49:26 +03:00
|
|
|
dc.Metadata[models.DomainUniqueName], dc.Name, dc.Metadata[models.DomainTag]),
|
2023-05-02 13:04:59 -04:00
|
|
|
)
|
2023-04-17 16:44:10 +02:00
|
|
|
|
2022-12-11 17:28:58 -05:00
|
|
|
// We only change the serial number if there is a change.
|
2023-03-22 08:46:36 -07:00
|
|
|
if !c.skipNextSoaIncrease {
|
|
|
|
desiredSoa.SoaSerial = nextSerial
|
|
|
|
}
|
2022-12-11 17:28:58 -05:00
|
|
|
|
|
|
|
corrections = append(corrections,
|
|
|
|
&models.Correction{
|
|
|
|
Msg: msg,
|
|
|
|
F: func() error {
|
|
|
|
printer.Printf("WRITING ZONEFILE: %v\n", c.zonefile)
|
2023-05-26 13:16:20 -04:00
|
|
|
fname, err := preprocessFilename(c.zonefile)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("could not create zonefile: %w", err)
|
|
|
|
}
|
|
|
|
zf, err := os.Create(fname)
|
2022-12-11 17:28:58 -05:00
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("could not create zonefile: %w", err)
|
|
|
|
}
|
|
|
|
// Beware that if there are any fake types, then they will
|
|
|
|
// be commented out on write, but we don't reverse that when
|
|
|
|
// reading, so there will be a diff on every invocation.
|
|
|
|
err = prettyzone.WriteZoneFileRC(zf, dc.Records, dc.Name, 0, comments)
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("failed WriteZoneFile: %w", err)
|
|
|
|
}
|
|
|
|
err = zf.Close()
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("closing: %w", err)
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
},
|
|
|
|
})
|
|
|
|
}
|
2022-12-11 15:02:58 -05:00
|
|
|
|
2016-08-22 18:31:50 -06:00
|
|
|
return corrections, nil
|
|
|
|
}
|
2023-05-26 13:16:20 -04:00
|
|
|
|
|
|
|
// preprocessFilename pre-processes a filename we're about to os.Create()
|
|
|
|
// * On Windows systems, it translates the seperator.
|
|
|
|
// * It attempts to mkdir the directories leading up to the filename.
|
|
|
|
// * If running on Linux as root, it does not attempt to create directories.
|
|
|
|
func preprocessFilename(name string) (string, error) {
|
|
|
|
universalName := filepath.FromSlash(name)
|
|
|
|
// Running as root? Don't create the parent directories. It is unsafe.
|
|
|
|
if os.Getuid() != 0 {
|
|
|
|
// Create the parent directories
|
|
|
|
dir := filepath.Dir(name)
|
|
|
|
universalDir := filepath.FromSlash(dir)
|
|
|
|
if err := os.MkdirAll(universalDir, 0750); err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return universalName, nil
|
|
|
|
}
|