github.com/jason-dour/hugo@v0.63.3/commands/static_syncer.go (about)

     1  // Copyright 2017 The Hugo Authors. All rights reserved.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  // http://www.apache.org/licenses/LICENSE-2.0
     7  //
     8  // Unless required by applicable law or agreed to in writing, software
     9  // distributed under the License is distributed on an "AS IS" BASIS,
    10  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    11  // See the License for the specific language governing permissions and
    12  // limitations under the License.
    13  
    14  package commands
    15  
    16  import (
    17  	"os"
    18  	"path/filepath"
    19  
    20  	"github.com/gohugoio/hugo/hugolib/filesystems"
    21  
    22  	"github.com/fsnotify/fsnotify"
    23  	"github.com/gohugoio/hugo/helpers"
    24  	"github.com/spf13/fsync"
    25  )
    26  
    27  type staticSyncer struct {
    28  	c *commandeer
    29  }
    30  
    31  func newStaticSyncer(c *commandeer) (*staticSyncer, error) {
    32  	return &staticSyncer{c: c}, nil
    33  }
    34  
    35  func (s *staticSyncer) isStatic(filename string) bool {
    36  	return s.c.hugo().BaseFs.SourceFilesystems.IsStatic(filename)
    37  }
    38  
    39  func (s *staticSyncer) syncsStaticEvents(staticEvents []fsnotify.Event) error {
    40  	c := s.c
    41  
    42  	syncFn := func(sourceFs *filesystems.SourceFilesystem) (uint64, error) {
    43  		publishDir := c.hugo().PathSpec.PublishDir
    44  		// If root, remove the second '/'
    45  		if publishDir == "//" {
    46  			publishDir = helpers.FilePathSeparator
    47  		}
    48  
    49  		if sourceFs.PublishFolder != "" {
    50  			publishDir = filepath.Join(publishDir, sourceFs.PublishFolder)
    51  		}
    52  
    53  		syncer := fsync.NewSyncer()
    54  		syncer.NoTimes = c.Cfg.GetBool("noTimes")
    55  		syncer.NoChmod = c.Cfg.GetBool("noChmod")
    56  		syncer.ChmodFilter = chmodFilter
    57  		syncer.SrcFs = sourceFs.Fs
    58  		syncer.DestFs = c.Fs.Destination
    59  
    60  		// prevent spamming the log on changes
    61  		logger := helpers.NewDistinctFeedbackLogger()
    62  
    63  		for _, ev := range staticEvents {
    64  			// Due to our approach of layering both directories and the content's rendered output
    65  			// into one we can't accurately remove a file not in one of the source directories.
    66  			// If a file is in the local static dir and also in the theme static dir and we remove
    67  			// it from one of those locations we expect it to still exist in the destination
    68  			//
    69  			// If Hugo generates a file (from the content dir) over a static file
    70  			// the content generated file should take precedence.
    71  			//
    72  			// Because we are now watching and handling individual events it is possible that a static
    73  			// event that occupies the same path as a content generated file will take precedence
    74  			// until a regeneration of the content takes places.
    75  			//
    76  			// Hugo assumes that these cases are very rare and will permit this bad behavior
    77  			// The alternative is to track every single file and which pipeline rendered it
    78  			// and then to handle conflict resolution on every event.
    79  
    80  			fromPath := ev.Name
    81  
    82  			relPath := sourceFs.MakePathRelative(fromPath)
    83  
    84  			if relPath == "" {
    85  				// Not member of this virtual host.
    86  				continue
    87  			}
    88  
    89  			// Remove || rename is harder and will require an assumption.
    90  			// Hugo takes the following approach:
    91  			// If the static file exists in any of the static source directories after this event
    92  			// Hugo will re-sync it.
    93  			// If it does not exist in all of the static directories Hugo will remove it.
    94  			//
    95  			// This assumes that Hugo has not generated content on top of a static file and then removed
    96  			// the source of that static file. In this case Hugo will incorrectly remove that file
    97  			// from the published directory.
    98  			if ev.Op&fsnotify.Rename == fsnotify.Rename || ev.Op&fsnotify.Remove == fsnotify.Remove {
    99  				if _, err := sourceFs.Fs.Stat(relPath); os.IsNotExist(err) {
   100  					// If file doesn't exist in any static dir, remove it
   101  					toRemove := filepath.Join(publishDir, relPath)
   102  
   103  					logger.Println("File no longer exists in static dir, removing", toRemove)
   104  					_ = c.Fs.Destination.RemoveAll(toRemove)
   105  				} else if err == nil {
   106  					// If file still exists, sync it
   107  					logger.Println("Syncing", relPath, "to", publishDir)
   108  
   109  					if err := syncer.Sync(filepath.Join(publishDir, relPath), relPath); err != nil {
   110  						c.logger.ERROR.Println(err)
   111  					}
   112  				} else {
   113  					c.logger.ERROR.Println(err)
   114  				}
   115  
   116  				continue
   117  			}
   118  
   119  			// For all other event operations Hugo will sync static.
   120  			logger.Println("Syncing", relPath, "to", publishDir)
   121  			if err := syncer.Sync(filepath.Join(publishDir, relPath), relPath); err != nil {
   122  				c.logger.ERROR.Println(err)
   123  			}
   124  		}
   125  
   126  		return 0, nil
   127  	}
   128  
   129  	_, err := c.doWithPublishDirs(syncFn)
   130  	return err
   131  
   132  }