Java'da bir dosyayı kopyalamanın standart özlü yolu?


421

Java'da bir dosyayı kopyalamanın tek yolunun akışları açmak, bir arabellek bildirmek, bir dosyada okumak, içinde döngü yapmak ve diğer buhara yazmaktan her zaman beni rahatsız etti. Ağ, bu tür bir çözümün benzer, ancak yine de biraz farklı uygulamalarıyla doludur.

Java dilinin sınırları içinde kalmanın daha iyi bir yolu var mı (işletim sistemine özgü komutların yürütülmesini içermiyor)? Belki de en azından bu temel uygulamayı gizleyen ve tek satırlık bir çözüm sağlayan güvenilir bir açık kaynak yardımcı program paketinde?


5
Apache Commons FileUtils'te , özellikle copyFile yöntemlerinde bir şey olabilir .
araç seti

22
Java 7 kullanıyorsanız, @GlenBest tarafından önerilen şekilde Files.copy kullanın: stackoverflow.com/a/16600787/44737
rob

Yanıtlar:


274

Araç setinin yukarıda bahsettiği gibi, Apache Commons IO, özellikle FileUtils gibi bir yoldur . copyFile () ; tüm ağır kaldırma işlemlerini sizin yerinize yapar.

Postscript olarak FileUtils'in son sürümlerinin (2.0.1 sürümü gibi) dosyaları kopyalamak için NIO kullanımını eklediğini unutmayın; NIO rutinleri, Java katmanı üzerinden baytları okuyarak ve yazarak işlemek yerine doğrudan OS / dosya sistemine kopyalamayı ertelediğinden, NIO dosya kopyalama performansını önemli ölçüde artırabilir . Performans arıyorsanız, FileUtils'in son sürümünü kullandığınızı kontrol etmeye değer olabilir.


1
Çok yardımcı - resmi bir sürümün bu nio değişikliklerini ne zaman dahil edeceğine dair bir fikriniz var mı?
Peter

2
Apache Commons IO'nun halka açık sürümü hala 1.4'de, grrrrrrr
Peter

14
Aralık 2010 itibariyle, Apache Commons IO, NIO işlevselliğine sahip 2.0.1'de. Yanıt güncellendi.
Simon Nickerson

4
Android kullanıcıları için bir uyarı: Bu, standart Android API'larına dahil DEĞİLDİR
IlDan

18
Java 7 veya daha yenisini kullanıyorsanız, @GlenBest tarafından önerildiği gibi Files.copy'yi kullanabilirsiniz: stackoverflow.com/a/16600787/44737
rob

278

Apache ortakları gibi mega api kullanmaktan kaçınırdım. Bu basit bir işlemdir ve yeni NIO paketindeki JDK'da yerleşiktir. Önceki bir cevapta zaten bir tür bağlantı vardı, ancak NIO api'deki anahtar yöntem yeni transferler "transferTo" ve "transferFrom".

http://java.sun.com/javase/6/docs/api/java/nio/channels/FileChannel.html#transferTo(long,%20long,%20java.nio.channels.WritableByteChannel)

Bağlantılı makalelerden biri, transferFrom'u kullanarak bu işlevi kodunuza nasıl entegre edeceğiniz konusunda harika bir yol gösterir:

public static void copyFile(File sourceFile, File destFile) throws IOException {
    if(!destFile.exists()) {
        destFile.createNewFile();
    }

    FileChannel source = null;
    FileChannel destination = null;

    try {
        source = new FileInputStream(sourceFile).getChannel();
        destination = new FileOutputStream(destFile).getChannel();
        destination.transferFrom(source, 0, source.size());
    }
    finally {
        if(source != null) {
            source.close();
        }
        if(destination != null) {
            destination.close();
        }
    }
}

NIO'yu öğrenmek biraz zor olabilir, bu yüzden gidip NIO'yu bir gecede öğrenmeye çalışmadan önce bu tamirciye güvenmek isteyebilirsiniz. Kişisel deneyimden, deneyiminiz yoksa ve java.io akışları aracılığıyla IO'ya tanıtıldıysa öğrenmek çok zor bir şey olabilir.


2
Teşekkürler, faydalı bilgiler. Ben hala Apache Commons gibi bir şey için, özellikle altında nio (düzgün) kullanıyorsa tartışmak istiyorum; ancak temelleri anlamanın önemli olduğunu kabul ediyorum.
Peter

1
Ne yazık ki, uyarılar var. Windows 7, 32 bit'te 1.5 Gb dosyasını kopyaladığımda, dosya eşlenemedi. Başka bir çözüm aramak zorunda kaldım.
Anton K.12

15
Yukarıdaki kodla ilgili üç olası sorun: (a) getChannel bir istisna atarsa, açık bir akış sızdırabilir; (b) büyük dosyalar için, işletim sisteminin işleyebileceğinden daha fazlasını aktarmaya çalışıyor olabilirsiniz; (c) transferFrom öğesinin dönüş değerini yoksayarsanız, dosyanın yalnızca bir bölümünü kopyalıyor olabilirsiniz. Bu yüzden org.apache.tools.ant.util.ResourceUtils.copyResource çok karmaşıktır. Ayrıca transferFrom tamam olsa da, transferTo'nun Linux'ta JDK 1.4'ü bozduğunu unutmayın
Jesse Glick

7
Bu güncellenmiş sürümün bu endişeleri giderdiğine
Mark Renouf

11
Bu kodun büyük bir sorunu var. transferTo () bir döngü içinde çağrılmalıdır. İstenen tutarın tamamını transfer etmeyi garanti etmez.
Lorne Marquis

180

Artık Java 7 ile, aşağıdaki kaynakla dene sözdizimini kullanabilirsiniz:

public static void copyFile( File from, File to ) throws IOException {

    if ( !to.exists() ) { to.createNewFile(); }

    try (
        FileChannel in = new FileInputStream( from ).getChannel();
        FileChannel out = new FileOutputStream( to ).getChannel() ) {

        out.transferFrom( in, 0, in.size() );
    }
}

Veya daha da iyisi, Java 7'de tanıtılan yeni Dosyalar sınıfı kullanılarak da gerçekleştirilebilir:

public static void copyFile( File from, File to ) throws IOException {
    Files.copy( from.toPath(), to.toPath() );
}

Oldukça şık, ha?


15
Java'nın bugüne kadar böyle bir şey eklememesi şaşırtıcı. Bazı işlemler bilgisayar yazılımı yazmanın mutlak temelidir. Oracle'ın Java geliştiricileri, yeni hizmetlerin taşınmasını kolaylaştırmak için hangi hizmetleri sunduklarına bakarak işletim sistemlerinden bir veya iki şey öğrenebilirler.
Rick Hodgin

2
Ah teşekkürler! Tüm yardımcı işlevleriyle yeni "Dosyalar" sınıfının farkında değildim. Tam olarak ihtiyacım olan şey var. Örnek için teşekkürler.
ChrisCantrell

1
Performans akıllıca, java MİT FileChannel iyidir, bu makale okumak journaldev.com/861/4-ways-to-copy-file-in-java
Pankaj

5
Bu kodun büyük bir sorunu var. transferTo () bir döngü içinde çağrılmalıdır. İstenen tutarın tamamını transfer etmeyi garanti etmez.
Lorne Marquis

@Scott: Pete tek satırlık bir çözüm istedi ve çok yakınsınız ... Files.copy dosyasını bir copyFile yönteminde sarmak gereksizdir. Ben sadece Files.copy (Path, Path to) cevabınızın başına koymak ve mevcut File nesneleri varsa File.toPath () kullanabilirsiniz bahsetmek istiyorum: Files.copy (fromFile.toPath (), toFile.toPath ())
soymak

89
  • Bu yöntemler performansa göre tasarlanmıştır (işletim sistemi yerel G / Ç ile bütünleşir).
  • Bu yöntemler dosyalar, dizinler ve bağlantılar ile çalışır.
  • Sağlanan seçeneklerin her biri dışarıda bırakılabilir - isteğe bağlıdır.

Yardımcı sınıf

package com.yourcompany.nio;

class Files {

    static int copyRecursive(Path source, Path target, boolean prompt, CopyOptions options...) {
        CopyVisitor copyVisitor = new CopyVisitor(source, target, options).copy();
        EnumSet<FileVisitOption> fileVisitOpts;
        if (Arrays.toList(options).contains(java.nio.file.LinkOption.NOFOLLOW_LINKS) {
            fileVisitOpts = EnumSet.noneOf(FileVisitOption.class) 
        } else {
            fileVisitOpts = EnumSet.of(FileVisitOption.FOLLOW_LINKS);
        }
        Files.walkFileTree(source[i], fileVisitOpts, Integer.MAX_VALUE, copyVisitor);
    }

    private class CopyVisitor implements FileVisitor<Path>  {
        final Path source;
        final Path target;
        final CopyOptions[] options;

        CopyVisitor(Path source, Path target, CopyOptions options...) {
             this.source = source;  this.target = target;  this.options = options;
        };

        @Override
        FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) {
        // before visiting entries in a directory we copy the directory
        // (okay if directory already exists).
        Path newdir = target.resolve(source.relativize(dir));
        try {
            Files.copy(dir, newdir, options);
        } catch (FileAlreadyExistsException x) {
            // ignore
        } catch (IOException x) {
            System.err.format("Unable to create: %s: %s%n", newdir, x);
            return SKIP_SUBTREE;
        }
        return CONTINUE;
    }

    @Override
    public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
        Path newfile= target.resolve(source.relativize(file));
        try {
            Files.copy(file, newfile, options);
        } catch (IOException x) {
            System.err.format("Unable to copy: %s: %s%n", source, x);
        }
        return CONTINUE;
    }

    @Override
    public FileVisitResult postVisitDirectory(Path dir, IOException exc) {
        // fix up modification time of directory when done
        if (exc == null && Arrays.toList(options).contains(COPY_ATTRIBUTES)) {
            Path newdir = target.resolve(source.relativize(dir));
            try {
                FileTime time = Files.getLastModifiedTime(dir);
                Files.setLastModifiedTime(newdir, time);
            } catch (IOException x) {
                System.err.format("Unable to copy all attributes to: %s: %s%n", newdir, x);
            }
        }
        return CONTINUE;
    }

    @Override
    public FileVisitResult visitFileFailed(Path file, IOException exc) {
        if (exc instanceof FileSystemLoopException) {
            System.err.println("cycle detected: " + file);
        } else {
            System.err.format("Unable to copy: %s: %s%n", file, exc);
        }
        return CONTINUE;
    }
}

Bir dizini veya dosyayı kopyalama

long bytes = java.nio.file.Files.copy( 
                 new java.io.File("<filepath1>").toPath(), 
                 new java.io.File("<filepath2>").toPath(),
                 java.nio.file.StandardCopyOption.REPLACE_EXISTING,
                 java.nio.file.StandardCopyOption.COPY_ATTRIBUTES,
                 java.nio.file.LinkOption.NOFOLLOW_LINKS);

Bir dizini veya dosyayı taşıma

long bytes = java.nio.file.Files.move( 
                 new java.io.File("<filepath1>").toPath(), 
                 new java.io.File("<filepath2>").toPath(),
                 java.nio.file.StandardCopyOption.ATOMIC_MOVE,
                 java.nio.file.StandardCopyOption.REPLACE_EXISTING);

Bir dizini veya dosyayı özyinelemeli olarak kopyalama

long bytes = com.yourcompany.nio.Files.copyRecursive( 
                 new java.io.File("<filepath1>").toPath(), 
                 new java.io.File("<filepath2>").toPath(),
                 java.nio.file.StandardCopyOption.REPLACE_EXISTING,
                 java.nio.file.StandardCopyOption.COPY_ATTRIBUTES
                 java.nio.file.LinkOption.NOFOLLOW_LINKS );

Dosyalar için paket adı yanlıştı (java.nio değil java.nio.file olmalıdır). Bunun için bir düzenleme gönderdim; umarım bu tamamdır!
Stuart Rossiter

43

Java 7'de kolaydır ...

File src = new File("original.txt");
File target = new File("copy.txt");

Files.copy(src.toPath(), target.toPath(), StandardCopyOption.REPLACE_EXISTING);

1
Cevabınız Scott veya Glen'in cevabına ne katıyor?
Uri Agassi

11
Kısacası, daha azı daha fazladır. Yanıtları iyi ve ayrıntılı, ama bakarken özledim. Ne yazık ki buna birçok cevap var ve birçoğu uzun, eski ve karmaşık ve Scott ve Glen'in iyi cevapları kayboldu (buna yardımcı olmak için upvotes vereceğim). Cevabım, var () ve hata mesajını çalarak üç satıra indirilerek iyileştirilip iyileştirilemeyeceğini merak ediyorum.
Kevin Sadler

Bu dizinler için geçerli değildir. Lanet herkes bunu yanlış anlıyor. Daha fazla API iletişim sorunu hatanız. Ben de yanlış anladım.
aaa

2
momo soru nasıl bir dosya kopyalamak oldu.
Kevin Sadler

28

Bir dosyayı kopyalayıp hedef yolunuza kaydetmek için aşağıdaki yöntemi kullanabilirsiniz.

public void copy(File src, File dst) throws IOException {
    InputStream in = new FileInputStream(src);
    try {
        OutputStream out = new FileOutputStream(dst);
        try {
            // Transfer bytes from in to out
            byte[] buf = new byte[1024];
            int len;
            while ((len = in.read(buf)) > 0) {
                out.write(buf, 0, len);
            }
        } finally {
            out.close();
        }
    } finally {
        in.close();
    }
}

1
Bu işe yarayacak, ama buradaki diğer cevaplardan daha iyi olduğunu sanmıyorum?
Rup

2
@Rup O (a) önemli ölçüde daha iyi burada diğer cevaplar daha çünkü o çalışır ve (b) üçüncü parti yazılım dayanmaz çünkü.
Lorne Marquis

1
@EJP Tamam, ama çok akıllı değil. Dosya kopyalama, bir uygulama işlemi değil, bir işletim sistemi veya dosya sistemi işlemi olmalıdır: Java, bir kopyayı tespit edebilir ve bunu durdurduğunuzda dosyayı açıkça okumanız dışında bir OS işlemine dönüştürebilir. Java'nın bunu yapabileceğini düşünmüyorsanız, 1K okuma ve yazma işlemlerini daha büyük bloklara optimize etmeye güvenir misiniz? Kaynak ve hedef yavaş bir ağ üzerinden uzak bir paylaşımdaysa, bu gereksiz bir iştir. Evet, bazı üçüncü taraf JAR'lar aptalca büyük (Guava!) Ama düzgün bir şekilde yapıldıkları gibi birçok şey ekliyorlar.
Rup

Bir cazibe gibi çalıştı. Üçüncü taraf kütüphaneleri gerektirmeyen ve java 1.6 üzerinde çalışan en iyi çözüm. Teşekkürler.
James Wierzba

@ Bir işletim sistemi işlevi olması gerektiğine katılıyorum, ancak yorumunuz hakkında başka bir anlam ifade edemiyorum. İlk kolondan sonraki kısımda bir yerde fiil bulunmuyor; Ben de 'güven' Java'nın 1k blokları daha büyük bir şeye dönüştürmesini beklemezdim, ancak kesinlikle kendimi çok daha büyük bloklar kullanacaktım; Asla paylaşılan dosyaları kullanan bir uygulama yazmam; ve büyük olasılıkla daha büyük bir arabellek kullanmak dışında, herhangi bir üçüncü taraf kütüphanesinin bu koddan daha 'uygun' bir şey yaptığını bilmiyorum.
Lorne Marquis

24

Tüm bu mekanizmaların izinler gibi meta verileri değil, yalnızca dosyanın içeriğini kopyaladığını unutmayın. Bu nedenle, linux üzerindeki yürütülebilir bir .sh dosyasını kopyalar veya taşırsanız, yeni dosya yürütülebilir olmaz.

Bir dosyayı gerçekten kopyalamak veya taşımak için, yani bir komut satırından kopyalamakla aynı sonucu elde etmek için, aslında yerel bir araç kullanmanız gerekir. Bir kabuk komut dosyası veya JNI.

Görünüşe göre, bu java 7'de düzeltilebilir - http://today.java.net/pub/a/today/2008/07/03/jsr-203-new-file-apis.html . Parmaklar geçti!


23

Google'ın Guava kütüphanesinde de bir kopyalama yöntemi vardır :

genel statik geçersiz kopya ( Dosyadan  ,
                         Dosyaya  )IOException 
                 atar
Tüm baytları bir dosyadan diğerine kopyalar.

Uyarı:to Varolan bir dosyayı temsil ediyorsa , dosyanın üzerine yazılır from. Eğer tove fromatıfta aynı dosyada, o dosyanın içeriğini silinecektir.

Parametreler:from - kaynak dosya to- hedef dosya

Atar: IOException - Bir G / Ç hatası oluşursa IllegalArgumentException-from.equals(to)



7

Yukarıdaki kodla ilgili üç olası sorun:

  1. GetChannel bir istisna atarsa, açık bir akış sızdırabilirsiniz.
  2. Büyük dosyalar için, işletim sisteminin işleyebileceğinden daha fazlasını aktarmaya çalışıyor olabilirsiniz.
  3. TransferFrom öğesinin dönüş değerini yok sayıyorsunuz, bu nedenle dosyanın yalnızca bir bölümünü kopyalıyor olabilir.

Bu yüzden org.apache.tools.ant.util.ResourceUtils.copyResourceçok karmaşık. Ayrıca transferFrom tamam olsa da, transferTo'nun Linux'ta JDK 1.4'ü bozduğunu unutmayın (bkz. Hata Kimliği: 5056395 ) - Jesse Glick Jan


7

Zaten Spring'i kullanan bir web uygulamasındaysanız ve basit dosya kopyalama için Apache Commons IO'yu dahil etmek istemiyorsanız , Spring çerçevesinin FileCopyUtils'ini kullanabilirsiniz .


7

İşte dosyaları tek bir kod satırı ile kolayca kopyalamanın üç yolu!

Java7 :

java.nio.file.Files # kopya

private static void copyFileUsingJava7Files(File source, File dest) throws IOException {
    Files.copy(source.toPath(), dest.toPath());
}

Appache Commons IO :

FileUtils # copyFile

private static void copyFileUsingApacheCommonsIO(File source, File dest) throws IOException {
    FileUtils.copyFile(source, dest);
}

Guava :

Dosyalar # kopya

private static void copyFileUsingGuava(File source,File dest) throws IOException{
    Files.copy(source,dest);          
}

Birincisi dizinler için çalışmıyor. Lanet herkes bunu yanlış anlıyor. Daha fazla API iletişim sorunu hatanız. Ben de yanlış anladım.
mmm

Birincisi 3 parametreye ihtiyaç duyar. Files.copySadece 2 parametreleri kullanarak içindir Pathetmek Stream. Sadece parametre eklemek StandardCopyOption.COPY_ATTRIBUTESveya StandardCopyOption.REPLACE_EXISTINGiçin PathiçinPath
Pimp Trizkit

6
public static void copyFile(File src, File dst) throws IOException
{
    long p = 0, dp, size;
    FileChannel in = null, out = null;

    try
    {
        if (!dst.exists()) dst.createNewFile();

        in = new FileInputStream(src).getChannel();
        out = new FileOutputStream(dst).getChannel();
        size = in.size();

        while ((dp = out.transferFrom(in, p, size)) > 0)
        {
            p += dp;
        }
    }
    finally {
        try
        {
            if (out != null) out.close();
        }
        finally {
            if (in != null) in.close();
        }
    }
}

Kabul edilen en iyi yanıttan farkı transferFrom'u bir süre döngü içinde almanız mı?
Rup

1
Derlemez ve createNewFile () çağrısı gereksiz ve israflıdır.
Lorne Marquis

3

Tamponlu NIO kopyası testime göre en hızlısıdır. Https://github.com/mhisoft/fastcopy adresindeki bir test projemden aşağıdaki çalışma koduna bakın

import java.io.Closeable;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.text.DecimalFormat;


public class test {

private static final int BUFFER = 4096*16;
static final DecimalFormat df = new DecimalFormat("#,###.##");
public static void nioBufferCopy(final File source, final File target )  {
    FileChannel in = null;
    FileChannel out = null;
    double  size=0;
    long overallT1 =  System.currentTimeMillis();

    try {
        in = new FileInputStream(source).getChannel();
        out = new FileOutputStream(target).getChannel();
        size = in.size();
        double size2InKB = size / 1024 ;
        ByteBuffer buffer = ByteBuffer.allocateDirect(BUFFER);

        while (in.read(buffer) != -1) {
            buffer.flip();

            while(buffer.hasRemaining()){
                out.write(buffer);
            }

            buffer.clear();
        }
        long overallT2 =  System.currentTimeMillis();
        System.out.println(String.format("Copied %s KB in %s millisecs", df.format(size2InKB),  (overallT2 - overallT1)));
    }
    catch (IOException e) {
        e.printStackTrace();
    }

    finally {
        close(in);
        close(out);
    }
}

private static void close(Closeable closable)  {
    if (closable != null) {
        try {
            closable.close();
        } catch (IOException e) {
            if (FastCopy.debug)
                e.printStackTrace();
        }    
    }
}

}


Güzel! bu standart java.io akışı yerine hızlı .. 10GB sadece 160 saniyede kopyalanıyor
aswzen

2

Hızlı ve aynı zamanda Android tüm Java sürümleri ile çalışmak:

private void copy(final File f1, final File f2) throws IOException {
    f2.createNewFile();

    final RandomAccessFile file1 = new RandomAccessFile(f1, "r");
    final RandomAccessFile file2 = new RandomAccessFile(f2, "rw");

    file2.getChannel().write(file1.getChannel().map(FileChannel.MapMode.READ_ONLY, 0, f1.length()));

    file1.close();
    file2.close();
}

1
Tüm dosya sistemleri bellek eşlemeli dosyaları desteklemez ve bence küçük dosyalar için nispeten pahalı.
Rup

1.4'ten önceki herhangi bir Java sürümü ile çalışmaz ve tek bir yazmanın yeterli olduğunu garanti eden hiçbir şey yoktur.
Lorne Marquis

1

Partiye biraz geç, ama burada çeşitli dosya kopyalama yöntemlerini kullanarak bir dosyayı kopyalamak için harcanan zamanın bir karşılaştırması. Ben 10 kez yöntemleri üzerinden döngü ve bir ortalama aldı. G / Ç akışlarını kullanarak dosya aktarımı en kötü aday gibi görünüyor:

Çeşitli yöntemlerle dosya aktarımının karşılaştırılması

İşte yöntemler:

private static long fileCopyUsingFileStreams(File fileToCopy, File newFile) throws IOException {
    FileInputStream input = new FileInputStream(fileToCopy);
    FileOutputStream output = new FileOutputStream(newFile);
    byte[] buf = new byte[1024];
    int bytesRead;
    long start = System.currentTimeMillis();
    while ((bytesRead = input.read(buf)) > 0)
    {
        output.write(buf, 0, bytesRead);
    }
    long end = System.currentTimeMillis();

    input.close();
    output.close();

    return (end-start);
}

private static long fileCopyUsingNIOChannelClass(File fileToCopy, File newFile) throws IOException
{
    FileInputStream inputStream = new FileInputStream(fileToCopy);
    FileChannel inChannel = inputStream.getChannel();

    FileOutputStream outputStream = new FileOutputStream(newFile);
    FileChannel outChannel = outputStream.getChannel();

    long start = System.currentTimeMillis();
    inChannel.transferTo(0, fileToCopy.length(), outChannel);
    long end = System.currentTimeMillis();

    inputStream.close();
    outputStream.close();

    return (end-start);
}

private static long fileCopyUsingApacheCommons(File fileToCopy, File newFile) throws IOException
{
    long start = System.currentTimeMillis();
    FileUtils.copyFile(fileToCopy, newFile);
    long end = System.currentTimeMillis();
    return (end-start);
}

private static long fileCopyUsingNIOFilesClass(File fileToCopy, File newFile) throws IOException
{
    Path source = Paths.get(fileToCopy.getPath());
    Path destination = Paths.get(newFile.getPath());
    long start = System.currentTimeMillis();
    Files.copy(source, destination, StandardCopyOption.REPLACE_EXISTING);
    long end = System.currentTimeMillis();

    return (end-start);
}

NIO kanal sınıfını kullanırken görebildiğim tek dezavantaj, hala ara dosya kopyalama ilerlemesini göstermenin bir yolunu bulamıyorum.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.