Простий спосіб копіювання файлу в Golang


78

Чи існує простий / швидкий спосіб копіювання файлу в Go?

Я не зміг знайти швидкий шлях у документах, і пошук в Інтернеті також не допомагає.


1
Люди також обговорюють копіювання вмісту файлу в інший під цим питанням stackoverflow.com/questions/1821811 / ...
user7610

Відповіді:


71

Попередження: Ця відповідь в основному стосується додавання другого посилання до файлу, а не копіювання вмісту.

Надійна і ефективна копія концептуально проста, але не просто реалізувати з - за необхідності обробляти ряд крайових випадків і системних обмежень, що накладаються на цільової операційної системи і його конфігурації.

Якщо ви просто хочете створити дублікат існуючого файлу, ви можете використовувати його os.Link(srcName, dstName). Це дозволяє уникнути необхідності переміщувати байти в додатку та економить місце на диску. Для великих файлів це істотна економія часу та місця.

Але різні операційні системи мають різні обмеження щодо роботи жорстких посилань. Залежно від програми та конфігурації цільової системи, Link()дзвінки можуть працювати не у всіх випадках.

Якщо вам потрібна одна загальна, надійна та ефективна функція копіювання, оновіть Copy()до:

  1. Виконайте перевірки, щоб переконатися, що принаймні якась форма копії буде успішною (дозволи на доступ, каталоги існують тощо)
  2. Перевірте, чи обидва файли вже існують і використовуються однаково os.SameFile, поверніть успіх, якщо вони однакові
  3. Спробуйте зв’язати, поверніть у разі успіху
  4. Скопіюйте байти (усі ефективні засоби не вдалися), поверніть результат

Оптимізацією було б скопіювати байти в режимі go, щоб абонент не блокував байтову копію. Це накладає додаткові складності на абонента для належного розгляду справи про успіх / помилку.

Якби я хотів обидва, я мав би дві різні функції копіювання: CopyFile(src, dst string) (error)для блокуючої копії і CopyFileAsync(src, dst string) (chan c, error)яка передає канал сигналізації назад абоненту для асинхронного випадку.

package main

import (
    "fmt"
    "io"
    "os"
)

// CopyFile copies a file from src to dst. If src and dst files exist, and are
// the same, then return success. Otherise, attempt to create a hard link
// between the two files. If that fail, copy the file contents from src to dst.
func CopyFile(src, dst string) (err error) {
    sfi, err := os.Stat(src)
    if err != nil {
        return
    }
    if !sfi.Mode().IsRegular() {
        // cannot copy non-regular files (e.g., directories,
        // symlinks, devices, etc.)
        return fmt.Errorf("CopyFile: non-regular source file %s (%q)", sfi.Name(), sfi.Mode().String())
    }
    dfi, err := os.Stat(dst)
    if err != nil {
        if !os.IsNotExist(err) {
            return
        }
    } else {
        if !(dfi.Mode().IsRegular()) {
            return fmt.Errorf("CopyFile: non-regular destination file %s (%q)", dfi.Name(), dfi.Mode().String())
        }
        if os.SameFile(sfi, dfi) {
            return
        }
    }
    if err = os.Link(src, dst); err == nil {
        return
    }
    err = copyFileContents(src, dst)
    return
}

// copyFileContents copies the contents of the file named src to the file named
// by dst. The file will be created if it does not already exist. If the
// destination file exists, all it's contents will be replaced by the contents
// of the source file.
func copyFileContents(src, dst string) (err error) {
    in, err := os.Open(src)
    if err != nil {
        return
    }
    defer in.Close()
    out, err := os.Create(dst)
    if err != nil {
        return
    }
    defer func() {
        cerr := out.Close()
        if err == nil {
            err = cerr
        }
    }()
    if _, err = io.Copy(out, in); err != nil {
        return
    }
    err = out.Sync()
    return
}

func main() {
    fmt.Printf("Copying %s to %s\n", os.Args[1], os.Args[2])
    err := CopyFile(os.Args[1], os.Args[2])
    if err != nil {
        fmt.Printf("CopyFile failed %q\n", err)
    } else {
        fmt.Printf("CopyFile succeeded\n")
    }
}

62
Слід додати велике попередження, що створення жорсткого посилання - це не те саме, що створення копії. З жорстким посиланням у вас є один файл, а з копією - два різних файли. Зміни першого файлу не вплинуть на другий файл при використанні копії.
topskip

1
Влучне зауваження. Я припустив, що це передбачається визначенням посилання, але насправді це зрозуміло лише тоді, коли це вже відомо.
markc

22
Питання було про копіювання файлу; не створюючи більше посилань на розділи до нього. Тверде посилання (або м’яке посилання) має бути альтернативною відповіддю, якщо користувач просто хоче посилатися на один і той самий файл із кількох місць.
Xeoncross

Теоретично, вам слід також перевірити, чи достатньо місця в зоні відпочинку.
edap

Майте на увазі, що через цю частину: if err = os.Link(src, dst)...Ця функція не працюватиме для резервного копіювання, як є. Якщо ви хочете скопіювати файли, щоб зробити резервну копію деяких даних, ви повинні скопіювати самі дані у файловій системі
Юрій Рочняк,

54

У вас є всі біти, необхідні для запису такої функції у стандартній бібліотеці. Ось очевидний код для цього.

// Copy the src file to dst. Any existing file will be overwritten and will not
// copy file attributes.
func Copy(src, dst string) error {
    in, err := os.Open(src)
    if err != nil {
        return err
    }
    defer in.Close()

    out, err := os.Create(dst)
    if err != nil {
        return err
    }
    defer out.Close()

    _, err = io.Copy(out, in)
    if err != nil {
        return err
    }
    return out.Close()
}

5
Залежно від програми, можливо, ви захочете помилитися, якщо вихідний файл існує, інакше ви перезапишете вміст файлу. Ви можете зробити це, викликавши os.OpenFile (dst, syscall.O_CREATE | syscall.O_EXCL, FileMode (0666)), а не os.Create (...). Цей виклик не вдасться, якщо цільовий файл уже існує. Іншою оптимізацією було б уникнути копіювання файлу, якщо два файли вже однакові (наприклад, якщо вони були зв’язані). Ви
Markc

Одним із аспектів, який неможливо досягти лише за допомогою стандартної бібліотеки, є прозора підтримка копіювання на запис, що може бути бажаним у деяких ситуаціях. Однак, cow підтримується лише в деяких файлових системах, і для нього не існує системного виклику (окрім ioctl).
kbolino

чи не out.Close()завжди відстрочені відмовлять? Ви не перевіряєте помилку, але в документації сказано, що послідовні дзвінки на Close()не зможуть.
Овеш

чому out.Close () у поверненні, а також у відстрочці?
Девід Джонс

@Ovesh, документ сказав "Закрити поверне помилку, якщо вона вже була викликана", повернення помилки відрізняється від помилки, а оператори defer ігнорують будь-яку повернуту помилку. Наявність відстрочки - це переконатися, що файл закривається, якщо під час io.Copy виникає помилка. Ви можете досягти того самого, закривши файл під час перевірки помилок io.COpy, але, можливо, менш елегантно.
user658991

12
import (
    "io/ioutil"
    "log"
)

func checkErr(err error) {
    if err != nil {
        log.Fatal(err)
    }
}

func copy(src string, dst string) {
    // Read all content of src to data
    data, err := ioutil.ReadFile(src)
    checkErr(err)
    // Write data to dst
    err = ioutil.WriteFile(dst, data, 0644)
    checkErr(err)
}

1
Не могли б ви додати кілька рядків пояснень або коментарів до свого коду, щоб допомогти ОП зрозуміти його.
Morten Jensen

3
Якщо в папці є файл розміром декількох концертів, ця програма з'їсть концерти пам'яті. Замість цього використовуйте io.CopyN ().
посмішка -

@ smile-on Так, але якщо ви копіюєте невеликий файл навколо для тесту чи чогось іншого, кому все одно? Варто, щоб тут був і швидко-брудний метод.
Кейсі

10

Якщо ви запускаєте код у Linux / mac, ви можете просто виконати команду cp системи.

srcFolder := "copy/from/path"
destFolder := "copy/to/path"
cpCmd := exec.Command("cp", "-rf", srcFolder, destFolder)
err := cpCmd.Run()

Це трактування схоже на сценарій, але це робить роботу. Крім того, вам потрібно імпортувати "os / exec"


9
Чи дає мені це гарантії того, що трапиться, якщо srcFolder або destFolder недійсні або навіть зловмисно створені користувачем? Скажіть destFolder: = "copy / to / path; rm -rf /", стиль введення SQL.
user7610

2
Якщо ви вказуєте вихідну та цільову папки від користувача, я рекомендую інший підхід. Цей код передбачає правильні шляхи.
Дандальф

5
@ user1047788 Хоча будь-який шлях від користувача потрібно очистити / перевірити, про всяк випадок, якщо вам цікаво, ";" не буде оцінюватися os.Exec як виконання нової команди. Ваш приклад фактично надішле точне значення "copy / to / path; rm -rf /" команді cp як аргумент (пробіли та інші символи включені).
Йоберт

Це акуратний трюк, який працює і на вікнах! Однак перехід на Windows зробить заміну імен у шляху srcFolder, а на Linux - ні. srcFolder: = "copy / from / path / *" ОК при перемозі, помилка в Linux.
посмішка -

1
Я спробував скопіювати файл --helpіз вашим кодом, і нічого не сталося. ;)
Роланд

1

У цьому випадку є кілька умов для перевірки, я віддаю перевагу невкладеному коду

func Copy(src, dst string) (int64, error) {
  src_file, err := os.Open(src)
  if err != nil {
    return 0, err
  }
  defer src_file.Close()

  src_file_stat, err := src_file.Stat()
  if err != nil {
    return 0, err
  }

  if !src_file_stat.Mode().IsRegular() {
    return 0, fmt.Errorf("%s is not a regular file", src)
  }

  dst_file, err := os.Create(dst)
  if err != nil {
    return 0, err
  }
  defer dst_file.Close()
  return io.Copy(dst_file, src_file)
}


-2

Ось очевидний спосіб скопіювати файл:

package main
import (
    "os"
    "log"
    "io"
)

func main() {
    sFile, err := os.Open("test.txt")
    if err != nil {
        log.Fatal(err)
    }
    defer sFile.Close()

    eFile, err := os.Create("test_copy.txt")
    if err != nil {
        log.Fatal(err)
    }
    defer eFile.Close()

    _, err = io.Copy(eFile, sFile) // first var shows number of bytes
    if err != nil {
        log.Fatal(err)
    }

    err = eFile.Sync()
    if err != nil {
        log.Fatal(err)
    }
}

Я спробував цей метод, але в результаті файл не працював належним чином.
snorberhuis

Що означає ein eFile?
Роланд

-3

Якщо у вас є вікна, ви можете обернути CopyFileW так:

package utils

import (
    "syscall"
    "unsafe"
)

var (
    modkernel32   = syscall.NewLazyDLL("kernel32.dll")
    procCopyFileW = modkernel32.NewProc("CopyFileW")
)

// CopyFile wraps windows function CopyFileW
func CopyFile(src, dst string, failIfExists bool) error {
    lpExistingFileName, err := syscall.UTF16PtrFromString(src)
    if err != nil {
        return err
    }

    lpNewFileName, err := syscall.UTF16PtrFromString(dst)
    if err != nil {
        return err
    }

    var bFailIfExists uint32
    if failIfExists {
        bFailIfExists = 1
    } else {
        bFailIfExists = 0
    }

    r1, _, err := syscall.Syscall(
        procCopyFileW.Addr(),
        3,
        uintptr(unsafe.Pointer(lpExistingFileName)),
        uintptr(unsafe.Pointer(lpNewFileName)),
        uintptr(bFailIfExists))

    if r1 == 0 {
        return err
    }
    return nil
}

Код натхненний обгортками C:\Go\src\syscall\zsyscall_windows.go


просто не має сенсу писати щось таке складне, коли мова надає всі необхідні примітиви
mh-cbon

він вирішує конкретний випадок використання, коли хочеться
підвищити

-3

Ви можете використовувати "exec". exec.Command ("cmd", "/ c", "copy", "fileToBeCopied destinationDirectory") для Windows, я використовував цей файл і працює нормально. Ви можете звернутися до керівництва для отримання детальної інформації про exec.


немає сенсу використовувати exec
mh-cbon

@ mh-cbon: Можливо, я помиляюся, поки я бачу, що це просто запитує спосіб копіювання файлів. Звичайно, можуть бути й інші шляхи, але я знайшов цей шлях дуже простим.
jigar137

-7

Погляньте на go-shutil .

Але майте на увазі, він не копіює метадані. Також потрібен хтось для реалізації таких речей, як переїзд.

Можливо, варто просто використовувати exec.


4
Я швидко переглянув зв’язаний пакет і не рекомендував би його. Хоча там сказано: "Ми не очікуємо, що він буде ідеальним, просто кращим, ніж будь-який ваш перший проект" ... вони помиляються. Вони роблять багато основних помилок, таких як ігнорування помилок, маючи багато перегонів (наприклад, перевіряючи, чи існує ім'я файлу джерела / місця призначення окремо від спроб пізніше відкрити / створити їх; ніколи цього не робіть !!) тощо
Дейв C

@DaveC Чи є у вас приклад для "такого як ігнорування помилок"? Я швидко переглянув код і не зміг помітити явної помилки в частині обробки помилок. Код не змінювався з 2014 року.
Роланд

1
@RolandIllig це було дуже давно, тому я не впевнений, на що саме я мав на увазі, але перевірка 30 секунд знайшла приклад за адресою github.com/termie/go-shutil/blob/master/shutil.go#L128 ; ніколи не ігноруйте помилки під час закриття файлу, до якого ви писали, багато разів помилка під час запису не відображається, поки дані не будуть очищені під час закриття. Враховуючи тон мого попереднього коментаря, я здогадуюсь, що я помітив і інші речі, коли я подивився на це більш серйозно в 2015 році.
Дейв С,
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.