func copyStatic() error { publishDir := helpers.AbsPathify(viper.GetString("PublishDir")) + helpers.FilePathSeparator // If root, remove the second '/' if publishDir == "//" { publishDir = helpers.FilePathSeparator } syncer := fsync.NewSyncer() syncer.NoTimes = viper.GetBool("notimes") syncer.SrcFs = hugofs.SourceFs syncer.DestFs = hugofs.DestinationFS themeDir, err := helpers.GetThemeStaticDirPath() if err != nil { jww.WARN.Println(err) } // Copy the theme's static directory if themeDir != "" { jww.INFO.Println("syncing from", themeDir, "to", publishDir) utils.CheckErr(syncer.Sync(publishDir, themeDir), fmt.Sprintf("Error copying static files of theme to %s", publishDir)) } // Copy the site's own static directory staticDir := helpers.GetStaticDirPath() + helpers.FilePathSeparator if _, err := os.Stat(staticDir); err == nil { jww.INFO.Println("syncing from", staticDir, "to", publishDir) return syncer.Sync(publishDir, staticDir) } else if os.IsNotExist(err) { jww.WARN.Println("Unable to find Static Directory:", staticDir) } return nil }
func getStaticSourceFs() afero.Fs { source := hugofs.SourceFs themeDir, err := helpers.GetThemeStaticDirPath() staticDir := helpers.GetStaticDirPath() + helpers.FilePathSeparator useTheme := true useStatic := true if err != nil { jww.WARN.Println(err) useTheme = false } else { if _, err := source.Stat(themeDir); os.IsNotExist(err) { jww.WARN.Println("Unable to find Theme Static Directory:", themeDir) useTheme = false } } if _, err := source.Stat(staticDir); os.IsNotExist(err) { jww.WARN.Println("Unable to find Static Directory:", staticDir) useStatic = false } if !useStatic && !useTheme { return nil } if !useStatic { jww.INFO.Println(themeDir, "is the only static directory available to sync from") return afero.NewReadOnlyFs(afero.NewBasePathFs(source, themeDir)) } if !useTheme { jww.INFO.Println(staticDir, "is the only static directory available to sync from") return afero.NewReadOnlyFs(afero.NewBasePathFs(source, staticDir)) } jww.INFO.Println("using a UnionFS for static directory comprised of:") jww.INFO.Println("Base:", themeDir) jww.INFO.Println("Overlay:", staticDir) base := afero.NewReadOnlyFs(afero.NewBasePathFs(hugofs.SourceFs, themeDir)) overlay := afero.NewReadOnlyFs(afero.NewBasePathFs(hugofs.SourceFs, staticDir)) return afero.NewCopyOnWriteFs(base, overlay) }
func getStaticSourceFs() afero.Fs { source := hugofs.SourceFs themeDir, err := helpers.GetThemeStaticDirPath() staticDir := helpers.GetStaticDirPath() + helpers.FilePathSeparator useTheme := true useStatic := true if err != nil { jww.WARN.Println(err) useTheme = false } else { if _, err := source.Stat(themeDir); os.IsNotExist(err) { jww.WARN.Println("Unable to find Theme Static Directory:", themeDir) useTheme = false } } if _, err := source.Stat(staticDir); os.IsNotExist(err) { jww.WARN.Println("Unable to find Static Directory:", staticDir) useStatic = false } if !useStatic && !useTheme { return nil } if !useStatic { return afero.NewReadOnlyFs(afero.NewBasePathFs(source, themeDir)) } if !useTheme { return afero.NewReadOnlyFs(afero.NewBasePathFs(source, staticDir)) } base := afero.NewReadOnlyFs(afero.NewBasePathFs(hugofs.SourceFs, themeDir)) overlay := afero.NewReadOnlyFs(afero.NewBasePathFs(hugofs.SourceFs, staticDir)) return afero.NewCopyOnWriteFs(base, overlay) }
// NewWatcher creates a new watcher to watch filesystem events. func NewWatcher(port int) error { if runtime.GOOS == "darwin" { tweakLimit() } watcher, err := watcher.New(1 * time.Second) var wg sync.WaitGroup if err != nil { return err } defer watcher.Close() wg.Add(1) for _, d := range getDirList() { if d != "" { _ = watcher.Add(d) } } go func() { for { select { case evs := <-watcher.Events: jww.INFO.Println("Received System Events:", evs) staticEvents := []fsnotify.Event{} dynamicEvents := []fsnotify.Event{} for _, ev := range evs { ext := filepath.Ext(ev.Name) istemp := strings.HasSuffix(ext, "~") || (ext == ".swp") || (ext == ".swx") || (ext == ".tmp") || (ext == ".DS_Store") || filepath.Base(ev.Name) == "4913" || strings.HasPrefix(ext, ".goutputstream") || strings.HasSuffix(ext, "jb_old___") || strings.HasSuffix(ext, "jb_bak___") if istemp { continue } // Sometimes during rm -rf operations a '"": REMOVE' is triggered. Just ignore these if ev.Name == "" { continue } // Write and rename operations are often followed by CHMOD. // There may be valid use cases for rebuilding the site on CHMOD, // but that will require more complex logic than this simple conditional. // On OS X this seems to be related to Spotlight, see: // https://github.com/go-fsnotify/fsnotify/issues/15 // A workaround is to put your site(s) on the Spotlight exception list, // but that may be a little mysterious for most end users. // So, for now, we skip reload on CHMOD. if ev.Op&fsnotify.Chmod == fsnotify.Chmod { continue } walkAdder := func(path string, f os.FileInfo, err error) error { if f.IsDir() { jww.FEEDBACK.Println("adding created directory to watchlist", path) watcher.Add(path) } return nil } // recursively add new directories to watch list // When mkdir -p is used, only the top directory triggers an event (at least on OSX) if ev.Op&fsnotify.Create == fsnotify.Create { if s, err := hugofs.SourceFs.Stat(ev.Name); err == nil && s.Mode().IsDir() { afero.Walk(hugofs.SourceFs, ev.Name, walkAdder) } } isstatic := strings.HasPrefix(ev.Name, helpers.GetStaticDirPath()) || (len(helpers.GetThemesDirPath()) > 0 && strings.HasPrefix(ev.Name, helpers.GetThemesDirPath())) if isstatic { staticEvents = append(staticEvents, ev) } else { dynamicEvents = append(dynamicEvents, ev) } } if len(staticEvents) > 0 { publishDir := helpers.AbsPathify(viper.GetString("PublishDir")) + helpers.FilePathSeparator // If root, remove the second '/' if publishDir == "//" { publishDir = helpers.FilePathSeparator } jww.FEEDBACK.Println("\nStatic file changes detected") const layout = "2006-01-02 15:04 -0700" fmt.Println(time.Now().Format(layout)) if viper.GetBool("ForceSyncStatic") { jww.FEEDBACK.Printf("Syncing all static files\n") err := copyStatic() if err != nil { utils.StopOnErr(err, fmt.Sprintf("Error copying static files to %s", helpers.AbsPathify(viper.GetString("PublishDir")))) } } else { staticSourceFs := getStaticSourceFs() if staticSourceFs == nil { jww.WARN.Println("No static directories found to sync") return } syncer := fsync.NewSyncer() syncer.NoTimes = viper.GetBool("notimes") syncer.SrcFs = staticSourceFs syncer.DestFs = hugofs.DestinationFS // prevent spamming the log on changes logger := helpers.NewDistinctFeedbackLogger() for _, ev := range staticEvents { // Due to our approach of layering both directories and the content's rendered output // into one we can't accurately remove a file not in one of the source directories. // If a file is in the local static dir and also in the theme static dir and we remove // it from one of those locations we expect it to still exist in the destination // // If Hugo generates a file (from the content dir) over a static file // the content generated file should take precedence. // // Because we are now watching and handling individual events it is possible that a static // event that occupies the same path as a content generated file will take precedence // until a regeneration of the content takes places. // // Hugo assumes that these cases are very rare and will permit this bad behavior // The alternative is to track every single file and which pipeline rendered it // and then to handle conflict resolution on every event. fromPath := ev.Name // If we are here we already know the event took place in a static dir relPath, err := helpers.MakeStaticPathRelative(fromPath) if err != nil { fmt.Println(err) continue } // Remove || rename is harder and will require an assumption. // Hugo takes the following approach: // If the static file exists in any of the static source directories after this event // Hugo will re-sync it. // If it does not exist in all of the static directories Hugo will remove it. // // This assumes that Hugo has not generated content on top of a static file and then removed // the source of that static file. In this case Hugo will incorrectly remove that file // from the published directory. if ev.Op&fsnotify.Rename == fsnotify.Rename || ev.Op&fsnotify.Remove == fsnotify.Remove { if _, err := staticSourceFs.Stat(relPath); os.IsNotExist(err) { // If file doesn't exist in any static dir, remove it toRemove := filepath.Join(publishDir, relPath) logger.Println("File no longer exists in static dir, removing", toRemove) hugofs.DestinationFS.RemoveAll(toRemove) } else if err == nil { // If file still exists, sync it logger.Println("Syncing", relPath, "to", publishDir) if err := syncer.Sync(filepath.Join(publishDir, relPath), relPath); err != nil { jww.ERROR.Println(err) } } else { jww.ERROR.Println(err) } continue } // For all other event operations Hugo will sync static. logger.Println("Syncing", relPath, "to", publishDir) if err := syncer.Sync(filepath.Join(publishDir, relPath), relPath); err != nil { jww.ERROR.Println(err) } } } if !buildWatch && !viper.GetBool("DisableLiveReload") { // Will block forever trying to write to a channel that nobody is reading if livereload isn't initialized // force refresh when more than one file if len(staticEvents) > 0 { for _, ev := range staticEvents { path, _ := helpers.MakeStaticPathRelative(ev.Name) livereload.RefreshPath(path) } } else { livereload.ForceRefresh() } } } if len(dynamicEvents) > 0 { fmt.Print("\nChange detected, rebuilding site\n") const layout = "2006-01-02 15:04 -0700" fmt.Println(time.Now().Format(layout)) rebuildSite(dynamicEvents) if !buildWatch && !viper.GetBool("DisableLiveReload") { // Will block forever trying to write to a channel that nobody is reading if livereload isn't initialized livereload.ForceRefresh() } } case err := <-watcher.Errors: if err != nil { fmt.Println("error:", err) } } } }() if port > 0 { if !viper.GetBool("DisableLiveReload") { livereload.Initialize() http.HandleFunc("/livereload.js", livereload.ServeJS) http.HandleFunc("/livereload", livereload.Handler) } go serve(port) } wg.Wait() return nil }
// NewWatcher creates a new watcher to watch filesystem events. func NewWatcher(port int) error { if runtime.GOOS == "darwin" { tweakLimit() } watcher, err := watcher.New(1 * time.Second) var wg sync.WaitGroup if err != nil { fmt.Println(err) return err } defer watcher.Close() wg.Add(1) for _, d := range getDirList() { if d != "" { _ = watcher.Add(d) } } go func() { for { select { case evs := <-watcher.Events: jww.INFO.Println("File System Event:", evs) staticChanged := false dynamicChanged := false staticFilesChanged := make(map[string]bool) for _, ev := range evs { ext := filepath.Ext(ev.Name) istemp := strings.HasSuffix(ext, "~") || (ext == ".swp") || (ext == ".swx") || (ext == ".tmp") || strings.HasPrefix(ext, ".goutputstream") if istemp { continue } // renames are always followed with Create/Modify if ev.Op&fsnotify.Rename == fsnotify.Rename { continue } isstatic := strings.HasPrefix(ev.Name, helpers.GetStaticDirPath()) || (len(helpers.GetThemesDirPath()) > 0 && strings.HasPrefix(ev.Name, helpers.GetThemesDirPath())) staticChanged = staticChanged || isstatic dynamicChanged = dynamicChanged || !isstatic if isstatic { if staticPath, err := helpers.MakeStaticPathRelative(ev.Name); err == nil { staticFilesChanged[staticPath] = true } } // add new directory to watch list if s, err := os.Stat(ev.Name); err == nil && s.Mode().IsDir() { if ev.Op&fsnotify.Create == fsnotify.Create { watcher.Add(ev.Name) } } } if staticChanged { jww.FEEDBACK.Printf("Static file changed, syncing\n\n") utils.StopOnErr(copyStatic(), fmt.Sprintf("Error copying static files to %s", helpers.AbsPathify(viper.GetString("PublishDir")))) if !BuildWatch && !viper.GetBool("DisableLiveReload") { // Will block forever trying to write to a channel that nobody is reading if livereload isn't initalized // force refresh when more than one file if len(staticFilesChanged) == 1 { for path := range staticFilesChanged { livereload.RefreshPath(path) } } else { livereload.ForceRefresh() } } } if dynamicChanged { fmt.Print("\nChange detected, rebuilding site\n") const layout = "2006-01-02 15:04 -0700" fmt.Println(time.Now().Format(layout)) utils.CheckErr(buildSite(true)) if !BuildWatch && !viper.GetBool("DisableLiveReload") { // Will block forever trying to write to a channel that nobody is reading if livereload isn't initalized livereload.ForceRefresh() } } case err := <-watcher.Errors: if err != nil { fmt.Println("error:", err) } } } }() if port > 0 { if !viper.GetBool("DisableLiveReload") { livereload.Initialize() http.HandleFunc("/livereload.js", livereload.ServeJS) http.HandleFunc("/livereload", livereload.Handler) } go serve(port) } wg.Wait() return nil }
// NewWatcher creates a new watcher to watch filesystem events. func NewWatcher(port int) error { if runtime.GOOS == "darwin" { tweakLimit() } watcher, err := watcher.New(1 * time.Second) var wg sync.WaitGroup if err != nil { return err } defer watcher.Close() wg.Add(1) for _, d := range getDirList() { if d != "" { _ = watcher.Add(d) } } go func() { for { select { case evs := <-watcher.Events: jww.INFO.Println("File System Event:", evs) staticChanged := false dynamicChanged := false staticFilesChanged := make(map[string]bool) for _, ev := range evs { ext := filepath.Ext(ev.Name) istemp := strings.HasSuffix(ext, "~") || (ext == ".swp") || (ext == ".swx") || (ext == ".tmp") || strings.HasPrefix(ext, ".goutputstream") || strings.HasSuffix(ext, "jb_old___") || strings.HasSuffix(ext, "jb_bak___") if istemp { continue } // renames are always followed with Create/Modify if ev.Op&fsnotify.Rename == fsnotify.Rename { continue } // Write and rename operations are often followed by CHMOD. // There may be valid use cases for rebuilding the site on CHMOD, // but that will require more complex logic than this simple conditional. // On OS X this seems to be related to Spotlight, see: // https://github.com/go-fsnotify/fsnotify/issues/15 // A workaround is to put your site(s) on the Spotlight exception list, // but that may be a little mysterious for most end users. // So, for now, we skip reload on CHMOD. if ev.Op&fsnotify.Chmod == fsnotify.Chmod { continue } isstatic := strings.HasPrefix(ev.Name, helpers.GetStaticDirPath()) || (len(helpers.GetThemesDirPath()) > 0 && strings.HasPrefix(ev.Name, helpers.GetThemesDirPath())) staticChanged = staticChanged || isstatic dynamicChanged = dynamicChanged || !isstatic if isstatic { staticFilesChanged[ev.Name] = true } // add new directory to watch list if s, err := os.Stat(ev.Name); err == nil && s.Mode().IsDir() { if ev.Op&fsnotify.Create == fsnotify.Create { watcher.Add(ev.Name) } } } if staticChanged { jww.FEEDBACK.Printf("Static file changed, syncing\n") if viper.GetBool("ForceSyncStatic") { jww.FEEDBACK.Printf("Syncing all static files\n") err := copyStatic() if err != nil { fmt.Println(err) utils.StopOnErr(err, fmt.Sprintf("Error copying static files to %s", helpers.AbsPathify(viper.GetString("PublishDir")))) } } else { syncer := fsync.NewSyncer() syncer.NoTimes = viper.GetBool("notimes") syncer.SrcFs = hugofs.SourceFs syncer.DestFs = hugofs.DestinationFS publishDir := helpers.AbsPathify(viper.GetString("PublishDir")) + helpers.FilePathSeparator if publishDir == "//" || publishDir == helpers.FilePathSeparator { publishDir = "" } staticDir := helpers.GetStaticDirPath() themeStaticDir := helpers.GetThemesDirPath() jww.FEEDBACK.Printf("StaticDir '%s'\nThemeStaticDir '%s'\n", staticDir, themeStaticDir) for path := range staticFilesChanged { var publishPath string if strings.HasPrefix(path, staticDir) { publishPath = filepath.Join(publishDir, strings.TrimPrefix(path, staticDir)) } else if strings.HasPrefix(path, themeStaticDir) { publishPath = filepath.Join(publishDir, strings.TrimPrefix(path, themeStaticDir)) } jww.FEEDBACK.Printf("Syncing file '%s'", path) if _, err := os.Stat(path); err == nil { jww.INFO.Println("syncing from ", path, " to ", publishPath) err := syncer.Sync(publishPath, path) if err != nil { jww.FEEDBACK.Printf("Error on syncing file '%s'\n", path) } } } } if !BuildWatch && !viper.GetBool("DisableLiveReload") { // Will block forever trying to write to a channel that nobody is reading if livereload isn't initalized // force refresh when more than one file if len(staticFilesChanged) == 1 { for path := range staticFilesChanged { livereload.RefreshPath(path) } } else { livereload.ForceRefresh() } } } if dynamicChanged { fmt.Print("\nChange detected, rebuilding site\n") const layout = "2006-01-02 15:04 -0700" fmt.Println(time.Now().Format(layout)) utils.CheckErr(buildSite(true)) if !BuildWatch && !viper.GetBool("DisableLiveReload") { // Will block forever trying to write to a channel that nobody is reading if livereload isn't initalized livereload.ForceRefresh() } } case err := <-watcher.Errors: if err != nil { fmt.Println("error:", err) } } } }() if port > 0 { if !viper.GetBool("DisableLiveReload") { livereload.Initialize() http.HandleFunc("/livereload.js", livereload.ServeJS) http.HandleFunc("/livereload", livereload.Handler) } go serve(port) } wg.Wait() return nil }