Coder Social home page Coder Social logo

perl6-archive-libarchive's Introduction

Actions Status

NAME

Archive::Libarchive - High-level bindings to libarchive

SYNOPSIS

use v6;

use Archive::Libarchive;
use Archive::Libarchive::Constants;

sub MAIN(:$file! where { .IO.f // die "file '$file' not found" })
{
  my Archive::Libarchive $a .= new:
      operation => LibarchiveExtract,
      file => $file,
      flags => ARCHIVE_EXTRACT_TIME +| ARCHIVE_EXTRACT_PERM +| ARCHIVE_EXTRACT_ACL +| ARCHIVE_EXTRACT_FFLAGS;
  try {
    $a.extract: sub (Archive::Libarchive::Entry $e --> Bool) { $e.pathname eq 'test2' };
    CATCH {
      say "Can't extract files: $_";
    }
  }
  $a.close;
}

For more examples see the example directory.

DESCRIPTION

Archive::Libarchive provides an OO interface to libarchive using Archive::Libarchive::Raw.

As the Libarchive site (http://www.libarchive.org/) states, its implementation is able to:

  • Read a variety of formats, including tar, pax, cpio, zip, xar, lha, ar, cab, mtree, rar, and ISO images.

  • Write tar, pax, cpio, zip, xar, ar, ISO, mtree, and shar archives.

  • Handle automatically archives compressed with gzip, bzip2, lzip, xz, lzma, or compress.

new(LibarchiveOp :$operation!, Any :$file?, Int :$flags?, Str :$format?, :@filters?)

Creates an Archive::Libarchive object. It takes one mandatory argument: operation, what kind of operation will be performed.

The list of possible operations is provided by the LibarchiveOp enum:

  • LibarchiveRead: open the archive to list its content.

  • LibarchiveWrite: create a new archive. The file must not be already present.

  • LibarchiveOverwrite: create a new archive. The file will be overwritten if present.

  • LibarchiveExtract: extract the archive content.

When extracting one can specify some options to be applied to the newly created files. The default options are:

ARCHIVE_EXTRACT_TIME +| ARCHIVE_EXTRACT_PERM +| ARCHIVE_EXTRACT_ACL +| ARCHIVE_EXTRACT_FFLAGS

Those constants are defined in Archive::Libarchive::Constants, part of the Archive::Libarchive::Raw distribution. More details about those operation modes can be found on the libarchive site: http://www.libarchive.org/

If the optional argument $file is provided, then it will be opened; if not provided during the initialization, the program must call the open method later.

If the optional $format argument is provided, then the object will select that specific format while dealing with the archive.

List of possible read formats:

  • 7zip

  • ar

  • cab

  • cpio

  • empty

  • gnutar

  • iso9660

  • lha

  • mtree

  • rar

  • raw

  • tar

  • warc

  • xar

  • zip

List of possible write formats:

  • 7zip

  • ar

  • cpio

  • gnutar

  • iso9660

  • mtree

  • pax

  • raw

  • shar

  • ustar

  • v7tar

  • warc

  • xar

  • zip

If the optional @filters parameter is provided, then the object will add those filter to the archive. Multiple filters can be specified, so a program can manage a file.tar.gz.uu for example. The order of the filters is significant, in order to correctly deal with such files as file.tar.uu.gz and file.tar.gz.uu.

List of possible read filters:

  • bzip2

  • compress

  • gzip

  • grzip

  • lrzip

  • lz4

  • lzip

  • lzma

  • lzop

  • none

  • rpm

  • uu

  • xz

List of possible write filters:

  • b64encode

  • bzip2

  • compress

  • grzip

  • gzip

  • lrzip

  • lz4

  • lzip

  • lzma

  • lzop

  • none

  • uuencode

  • xz

Note

Recent versions of libarchive implement an automatic way to determine the best mix of format and filters. If one's using a pretty recent libarchive, both $format and @filters may be omitted: the new method will determine automatically the right combination of parameters. Older versions though don't have that capability and the programmer has to define explicitly both parameters.

open(Str $filename!, Int :$size?, :$format?, :@filters?)

open(Buf $data!)

Opens an archive; the first form is used on files, while the second one is used to open an archive that resides in memory. The first argument is always mandatory, while the other ones might been omitted. $size is the size of the internal buffer and defaults to 10240 bytes.

Note: this module does't apply $*CWD to the file name under the hood, so this will create a file in the original directory.

use Archive::Libarchive;

my Archive::Libarchive $a .= new: operation => LibarchiveWrite;
chdir 'subdir';
$a.open: 'file.tar.gz', format => 'gnutar', filters => ['gzip'];
…

close

Closes the internal archive object, frees the memory and cleans up.

extract-opts(Int $flags?)

Sets the options for the files created when extracting files from an archive. The default options are:

ARCHIVE_EXTRACT_TIME +| ARCHIVE_EXTRACT_PERM +| ARCHIVE_EXTRACT_ACL +| ARCHIVE_EXTRACT_FFLAGS

next-header(Archive::Libarchive::Entry:D $e! --> Bool)

When reading an archive this method fills the Entry object and returns True till it reaches the end of the archive.

The Entry object is pubblicly defined inside the Archive::Libarchive module. It's initialized this way:

my Archive::Libarchive::Entry $e .= new;

So a complete archive lister can be implemented in few lines:

use Archive::Libarchive;

sub MAIN(Str :$file! where { .IO.f // die "file '$file' not found" })
{
  my Archive::Libarchive $a .= new: operation => LibarchiveRead, file => $file;
  my Archive::Libarchive::Entry $e .= new;
  while $a.next-header($e) {
    $e.pathname.say;
    $a.data-skip;
  }
  $a.close;
}

data-skip(--> Int)

When reading an archive this method skips file data to jump to the next header. It returns ARCHIVE_OK or ARCHIVE_EOF (defined in Archive::Libarchive::Constants)

read-file-content(Archive::Libarchive::Entry $e! --> Buf)

This method reads the content of a file represented by its Entry object and returns it.

write-header(Str $file, Str :$pathname?, Int :$size?, Int :$filetype?, Int :$perm?, Int :$atime?, Int :$mtime?, Int :$ctime?, Int :$birthtime?, Int :$uid?, Int :$gid?, Str :$uname?, Str :$gname? --> Bool)

When creating an archive this method writes the header entry for the file being inserted into the archive. The only mandatory argument is the file name, every other argument has a reasonable default. If the being inserted into the archive is a symbolic link, the target will be composed as a pathname relative to the base directory of the file, not as a full pathname. More details can be found on the libarchive site.

Each optional argument is available as a method of the Archive::Libarchive::Entry object and it can be set when needed.

Note write-header has a lot of optional arguments whose values are collected from the file one is adding to the archive. When using the second form of write-data one has to provide at least these arguments:

  • $size

  • $atime

  • $mtime

  • $ctime

For example:

$a.write-header($filename,
                :size($buffer.bytes),
                :atime(now.Int),
                :mtime(now.Int),
                :ctime(now.Int));

write-data(Str $path --> Bool)

write-data(Buf $data --> Bool)

When creating an archive this method writes the data for the file being inserted into the archive. $path is the pathname of the file to be archived, while $data is a data buffer.

extract(Str $destpath? --> Bool)

extract(&callback:(Archive::Libarchive::Entry $e --> Bool)!, Str $destpath? --> Bool)

When extracting files from an archive this method does all the dirty work. If used in the first form it extracts all the files. The second form takes a callback function, which receives a Archive::Libarchive::Entry object.

For example, this will extract only the file whose name is test2:

$a.extract: sub (Archive::Libarchive::Entry $e --> Bool) { $e.pathname eq 'test2' };

In both cases one can specify the directory into which the files will be extracted.

lib-version

Returns a hash with the version number of libarchive and of each library used internally.

Archive::Libarchive::Entry

This class encapsulate an entry of the archive. It provides the following methods.

pathname(Str $path)

pathname(--> Str)

Sets or gets a pathname.

size(Int $size)

size(--> int64)

Sets or gets the object's size.

filetype(Int $type)

filetype(--> int64)

filetype(:$decode where :so --> List)

Sets or gets the object's file type. If the first form of the getter is used a bit-mapped value is returned, that can be queried using the AE_* constants defined in Archive::Libarchive::Constants. If the second form of the getter is used a list is returned, which contain True or False values for each possible file type, listed in the following order:

  • regular file

  • directory

  • symbolic link

  • socket

  • character device

  • block device

  • fifo (named pipe)

This module exports several subs to test the file type:

is-file(Int $val --> Bool)

is-dir(Int $val --> Bool)

is-link(Int $val --> Bool)

is-sock(Int $val --> Bool)

is-chr(Int $val --> Bool)

is-blk(Int $val --> Bool)

is-fifo(Int $val --> Bool)

mode(Int $mode)

mode(--> int64)

Sets or gets the object's mode.

perm(Int $perm)

perm(--> int64)

Sets or gets the object's permissions.

atime(Int $atime)

atime(--> int64)

Sets or gets the object's access time.

ctime(Int $ctime)

ctime(--> int64)

Sets or gets the object's change time.

mtime(Int $mtime)

mtime(--> int64)

Sets or gets the object's modification time.

birthtime(Int $birthtime)

birthtime()

Sets or resets the object's birth time.

uid(Int $uid)

uid(--> int64)

Sets or gets the object's uid.

gid(Int $gid)

gid(--> int64)

Sets or gets the object's gid.

uname(Str $uname)

uname(--> Str)

Sets or gets the object's user name.

gname(Str $gname)

gname(--> Str)

Sets or gets the object's group name.

Errors

When the underlying library returns an error condition, the methods will return a Failure object, which can be trapped and the exception can be analyzed and acted upon.

The exception object has two fields: $errno and $error, and return a message stating the error number and the associated message as delivered by libarchive.

Prerequisites

This module requires the libarchive library to be installed. Please follow the instructions below based on your platform:

Debian Linux

sudo apt-get install libarchive13

The module uses Archive::Libarchive::Raw which looks for a library called libarchive.so.

Installation

To install it using zef (a module management tool):

$ zef update
$ zef install Archive::Libarchive

Author

Fernando Santagata

Contributions

Many thanks to Haythem Elganiny for implementing some multi methods in the Entry class.

License The Artistic License 2.0

perl6-archive-libarchive's People

Contributors

frithnanth avatar jnthn avatar niner avatar

Stargazers

 avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

perl6-archive-libarchive's Issues

Error 90: No such entry found

I have a tar.gz that contains two txt files, I try to print out the content of each file:

my $a = Archive::Libarchive.new: operation => LibarchiveRead, file => $payload;

my $log-content = $a.read-file-content(sub (Archive::Libarchive::Entry $e --> Bool) { $e.pathname.ends-with('_log.txt')    });
my $res-content = $a.read-file-content(sub (Archive::Libarchive::Entry $e --> Bool) { $e.pathname.ends-with('_result.txt') });

say $log-content.decode('UTF8-C8'); #
say $res-content.decode('UTF8-C8'); #

step print the content of file that end with _log.txt ok .

but step result in:

Error 90: No such entry found

if I comment the $log-content line, I can print out the content of _result.txt :

#  my $log-content = $a.read-file-content(sub (Archive::Libarchive::Entry $e --> Bool) { $e.pathname.ends-with('_log.txt')    }); # comment this 
my $res-content = $a.read-file-content(sub (Archive::Libarchive::Entry $e --> Bool) { $e.pathname.ends-with('_result.txt') }); # print out the content ok

[Feature Request] Extract to a particular directory

One thing I miss is being able to tell extract where exactly to extract things to. I was trying to use the library to replace an invocation like:

run "tar", "xfz", $archive, "-C", $local-temp;

Which extracts the files into $local-temp. Having done a little digging, it seems that libarchive doesn't doesn't actually support this very directly, but the answer here mentions a way to do it.

While I might get away with chdir it's going to be fragile/tricky. My application works concurrently (so multiple threads) but chdir is process-global. Perl 6 tries to avoid this problem by "simulating" chdir using $*CWD but that won't help a NativeCall'd library. We can really change the directory at OS level using &PROCESS::chdir which should change it from the perspective of libarchive. Unfortunately, given I have a concurrent application, then the moment I get two things going on in my application that need to &PROCESS::chdir, I'm going to have a very a bad time. :-) So I'd rather not introduce that.

Please let me know if this feature request seems reasonable. If you can implement it, even better...failing that I may find time for a patch (I'm also hoping to contribute something for making it easy to install this library on Windows, by fetching the DLL automatically like GTK::Simple does).

Symlinks support

Is symlink file type supported?

When archiving a symlink i get the link target data in the archive not symlink file type.

Unable to install with zef or Panda

Thanks for working on this module. I just tried to install it; with panda it fails as:

Failure Summary
----------------
Archive::Libarchive(
	*fetch stage failed for Archive::Libarchive::Raw: Unable to handle source 'https://github.com/frithnanth/perl6-Archive-Libarchive-Raw')
jnthn@lviv:~/edument/rmtly$ panda install Archive::Libarchive::Raw
==> Fetching Archive::Libarchive::Raw
fetch stage failed for Archive::Libarchive::Raw: Unable to handle source 'https://github.com/frithnanth/perl6-Archive-Libarchive-Raw'

And with zef a bit differently:

===> Searching for: Archive::Libarchive
===> Searching for missing dependencies: Archive::Libarchive::Raw
===> Fetching: Archive::Libarchive
No extracting backend available
  in method extract at /home/jnthn/dev/MoarVM/install/share/perl6/site/sources/F94CA4E91B2AF324B8A925E1F065F40258AE4D90 (Zef::Extract) line 8

At a guess, something is not quite right in the META.info for this module and/or its dependency, Archive::Libarchive::Raw.

Things don't work out with format `tar` and filters `none`

First I tired this:

my Archive::Libarchive $archive .= new(
    operation => LibarchiveOverwrite,
    file => $tar-file,
    format => 'tar'
);

This complained that I had to specify filters together with format. I didn't want any so I tried:

my Archive::Libarchive $archive .= new(
    operation => LibarchiveOverwrite,
    file => $tar-file
    format => 'tar',
    filters => ['none']
);

It then told me there was "No such format 'tar'", which is a tad odd (it's in the list of supported ones in the README). Happily, that was easily fixed by changing to gnutar:

my Archive::Libarchive $archive .= new(
    operation => LibarchiveOverwrite,
    file => $tar-file
    format => 'gnutar',
    filters => ['none']
);

This told me that none is not a valid filter. I didn't figure out how to solve that, but it turned out I could apply gzip compression, so I changed it to:

my Archive::Libarchive $archive .= new(
    operation => LibarchiveOverwrite,
    file => $tar-file
    format => 'gnutar',
    filters => ['gzip']
);

Which in this case was OK because I could also control the thing that would process the output to make it also ungzip it. With that, everything worked very nicely! :-)

So in summary, two issues:

  • The bigger issue: there seems to be no way to just get a tar without any filtering (it'd also be nice if the filter parameter would default to ['none'] when that is properly supported)
  • The smaller issue: tar didn't seem to work, I had to use guntar

Again, thanks for the module.

tests fail on macos 13.1 (ventura) on install

===> Building [OK] for Archive::Libarchive::Raw:ver<0.1.2>:auth<zef:FRITH>
===> Testing: Archive::Libarchive::Raw:ver<0.1.2>:auth<zef:FRITH>
[Archive::Libarchive::Raw] t/00-use.rakutest ................ ok
[Archive::Libarchive::Raw] t/01-version.rakutest ............ ok
[Archive::Libarchive::Raw] t/02-list.rakutest ............... Dubious, test returned 1
[Archive::Libarchive::Raw] No subtests run
[Archive::Libarchive::Raw] t/03-extract.rakutest ............ Dubious, test returned 1
[Archive::Libarchive::Raw] No subtests run
[Archive::Libarchive::Raw] t/04-archive.rakutest ............ Dubious, test returned 1
[Archive::Libarchive::Raw] No subtests run
[Archive::Libarchive::Raw] t/05-archive-read-disk.rakutest .. Dubious, test returned 255
[Archive::Libarchive::Raw] All 56 subtests passed
[Archive::Libarchive::Raw] t/99-meta.rakutest ............... ok
[Archive::Libarchive::Raw] All tests successful.
[Archive::Libarchive::Raw]
[Archive::Libarchive::Raw] Test Summary Report
[Archive::Libarchive::Raw] -------------------
[Archive::Libarchive::Raw] t/02-list.rakutest  (Wstat: 256 Tests: 0 Failed: 0)
[Archive::Libarchive::Raw] Non-zero exit status: 1
[Archive::Libarchive::Raw]   Parse errors: No plan found in TAP output
[Archive::Libarchive::Raw] t/03-extract.rakutest  (Wstat: 256 Tests: 0 Failed: 0)
[Archive::Libarchive::Raw] Non-zero exit status: 1
[Archive::Libarchive::Raw]   Parse errors: No plan found in TAP output
[Archive::Libarchive::Raw] t/04-archive.rakutest  (Wstat: 256 Tests: 0 Failed: 0)
[Archive::Libarchive::Raw] Non-zero exit status: 1
[Archive::Libarchive::Raw]   Parse errors: No plan found in TAP output
[Archive::Libarchive::Raw] t/05-archive-read-disk.rakutest (Wstat: 65280 Tests: 0 Failed: 0)
[Archive::Libarchive::Raw] Non-zero exit status: 255
[Archive::Libarchive::Raw]   Parse errors: Bad plan.  You planned 56 tests but ran 0.
[Archive::Libarchive::Raw] Files=7, Tests=10,  3 wallclock secs
[Archive::Libarchive::Raw] Result: FAILED
===> Testing [FAIL]: Archive::Libarchive::Raw:ver<0.1.2>:auth<zef:FRITH>
Aborting due to test failure: Archive::Libarchive::Raw:ver<0.1.2>:auth<zef:FRITH> (use --force-test to override)

Segfault when extracting

I just got a segfault when using this library to extract files. It's a simple use, on a .tar.gz file:

            given await($tarball-location) {
                my Archive::Libarchive $archive .= new(
                    operation => LibarchiveExtract,        
                    file => $_
                );
                $archive.extract;
                $archive.close;
                unlink $_;
            }

Running it under perl6-valgrind-m I see that it seems to be due to mis-use of libarchive somehow; it crashes when calling archive_entry_free:

==4199== Invalid write of size 8
==4199==    at 0x22C10164: archive_wstring_free (in /usr/lib/x86_64-linux-gnu/libarchive.so.13.1.2)
==4199==    by 0x22C13A4C: archive_mstring_clean (in /usr/lib/x86_64-linux-gnu/libarchive.so.13.1.2)
==4199==    by 0x22BD4BD8: archive_entry_clear (in /usr/lib/x86_64-linux-gnu/libarchive.so.13.1.2)
==4199==    by 0x22BD4C98: archive_entry_free (in /usr/lib/x86_64-linux-gnu/libarchive.so.13.1.2)
==4199==    by 0x50670DD: ??? (in /home/jnthn/dev/MoarVM/install/lib/libmoar.so)
==4199==    by 0x16125B8F: ???
==4199==    by 0x5067018: dc_callvm_call_x64 (in /home/jnthn/dev/MoarVM/install/lib/libmoar.so)
==4199==    by 0x22BD4C8F: ??? (in /usr/lib/x86_64-linux-gnu/libarchive.so.13.1.2)
==4199==    by 0x273148BF: ???
==4199==  Address 0xc56ac20 is 256 bytes inside a block of size 1,072 free'd
==4199==    at 0x4C2EDEB: free (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==4199==    by 0x22BDE1CC: ??? (in /usr/lib/x86_64-linux-gnu/libarchive.so.13.1.2)
==4199==    by 0x50670DD: ??? (in /home/jnthn/dev/MoarVM/install/lib/libmoar.so)
==4199==    by 0x16125B8F: ???
==4199==    by 0x5067018: dc_callvm_call_x64 (in /home/jnthn/dev/MoarVM/install/lib/libmoar.so)
==4199==    by 0x22C14A6F: ??? (in /usr/lib/x86_64-linux-gnu/libarchive.so.13.1.2)
==4199==    by 0x1034EC4F: ???
==4199==  Block was alloc'd at
==4199==    at 0x4C2FB55: calloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==4199==    by 0x22BD31D2: archive_entry_new2 (in /usr/lib/x86_64-linux-gnu/libarchive.so.13.1.2)
==4199==    by 0x22BDD608: archive_read_new (in /usr/lib/x86_64-linux-gnu/libarchive.so.13.1.2)
==4199==    by 0x50670DD: ??? (in /home/jnthn/dev/MoarVM/install/lib/libmoar.so)
==4199==    by 0x16125B8F: ???
==4199==    by 0x5067018: dc_callvm_call_x64 (in /home/jnthn/dev/MoarVM/install/lib/libmoar.so)
==4199==    by 0x22BDD5CF: ??? (in /usr/lib/x86_64-linux-gnu/libarchive.so.13.1.2)
==4199==    by 0x2640D55F: ???

Pathname too long

Getting Pathname too long error in method write-header when archiving GCC or libstdc++

Sample file that causes the error:

"/tmp/ii1HMTTEd8/gcc-9.1.0-x86_64-1-tagid/tools/include/c++/9.1.0/ext/pb_ds/detail/cc_hash_table_map_/constructor_destructor_no_store_hash_fn_imps.hpp".IO
Error 4294967271: Pathname too long

I'm not sure if this is really a long number of characters in pathname or may because of something to do with the characters them selves in the pathname, as I noticed that the files that causes this error is located inside directories that end with _

I will test again when get back home, since I just updated my work computer and that might have caused the issue.

Best way to get the filetype of an entry in an archive?

I'm reading the entries in an archive. I'm only interested in the entries that are file names, not directories.

I can use the filetype method on the entry object: $entry.filetype and I get what looks like an Int. I get 16384 for a directory and 32768 for a file.

But I don't know if these numbers might be different on a different machine. I looked for constants in the Raku module code but didn't find any.

Another possible way to check for whether a directory is a directory is to check to see if the last character is a /. But again, I'm not 100% sure this will work across all machine types.

It would be nice if the filetype method returned something more intuitive or there were some other method to get the type for an entry.

Thanks.

Is there a method/example to read the content of each entry in the archive?

The code below just print out the filenames in the archive:

my $a = Archive::Libarchive.new: operation => LibarchiveRead, file => $msg.payload;
my Archive::Libarchive::Entry $e .= new;
while $a.next-header($e) {
    $e.pathname.say;
    $a.data-skip;
}

which print out:

result.txt
log.txt

I want to read the content of result.txt and log.txt but don't want to uncompress the .tag.gz file. The method !copy-data seems the right answer, but it's a private method, so is there a method/example to read the content of each entry in the archive?

Archive not being written to CWD

When using Archive::Libarchive with ArchiveOverwrite, the expectation is that the archive is written relative to the CurrentWorkingDirectory, not relative to the directory where the Raku program was called from.

If it is a design decision to make the path of the Archive relative to the calling directory, and not $*CWD, may I suggest this is noted somewhere in the documentation.

Please see the following code

#!/usr/bin/env perl6
use v6.d;
use Archive::Libarchive;
use Archive::Libarchive::Constants;

my Archive::Libarchive $arc .= new(
    :operation(LibarchiveOverwrite),
    :file('cache.7z'),
);

my %h = <one two three four> Z=> 1..*;
# change to a sub-directory
my $orig = $*CWD;
chdir 'subdir';
my $b = Buf.new( %h.raku.encode );
say "Current dir: $*CWD";
try {
    $arc.write-header( 'some_name',
        :size($b.bytes),
        :atime(now.Int),
        :mtime(now.Int),
        :ctime(now.Int));
    $arc.write-data( $b );
    CATCH {
        default { .Str.say }
    }
}
$arc.close;
say 'finished';
$*CWD = $orig;
say 'listing of root: ', ('.'.IO.dir);
say 'listing of root/subdir: ', ('./subdir'.IO.dir);

which outputs

raku save-2-cache.raku 
Current dir: /home/richard/development/raku-collection/trial/subdir
finished
listing of root: ("get-from-cache.raku".IO "save-2-cache.raku".IO "cache.7z".IO "subdir".IO)
listing of root/subdir: ()

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.